今天面试遇到这么个问题:
有如下代码:
vector<int>f(){}
vector<int>ret=f();
假设函数f返回的vector很大,我们不想拷贝,但又想得到函数f返回的值怎么办?
我当时想到了移动,但是不清楚怎么用。
其实这里的答案很简单,vector<int>ret=f();
这句话本身就会调用vector的移动构造函数,不会发生拷贝。
为了弄清楚为什么,我做了以下实验
class A
{
public:
A(){}
//拷贝构造
A(const A& rhs) {
cout << "copy construct" << endl;
}
//移动构造
A(A &&a) {
cout << "move construct" << endl;
}
//拷贝赋值
A& operator=(const A &rhs) {
cout << "copy assign" << endl;
return *this;
}
//移动赋值
A& operator=(A &&rhs) {
cout << "move assign" << endl;
return *this;
}
};
A testa() {
A a;
return a;
}
int main()
{
A a1, a2;
a1=testa();
a2 = a1;
}
先放输出
移动构造发生在函数testa返回a的时候,使用移动构造函数创建一个临时对象。
然后再调用移动赋值运算符,把该临时对象赋值给a1。
这里有一个疑惑点,a是变量,变量是左值,难道不应该是调用拷贝构造函数构造返回的临时对象吗?
这点我没有在书上找到明确的答案,但是我推测原因可能是:a变量在执行完return语句之后,出了自身的作用域,即将被销毁,所以这时候把它的内容移动到返回的临时对象上是安全的,编译器基于此,做出这样的优化。
那么为什么a1=testa()
这里使用移动赋值而不是拷贝赋值呢?
原因在于testa()
是个临时对象,也就是右值,自然精确匹配到移动赋值运算符的参数列表A &&rhs
。
拷贝赋值发生在a2=a1
因为a1是左值,自然匹配到拷贝赋值运算符的const A &rhs
。如果想让a1移动到a2而不是拷贝过去的话,需要改成这样:
a2=std::move(a1);
std::move
得到a1的右值引用,这样就能精确匹配到类A的移动赋值运算符。
测试一下输出:
果然,使用了移动赋值。
如果把代码改成下面的:
int main()
{
A a1=testa();
}
输出结果是:
这里只调用了一次移动构造函数。发生在return a
的时候,直接把a移动到了a1。 而不是先移动到要返回的临时对象,然后再移动到a1。这里我也把它看成是编译器的优化。
此外,如果把移动构造和移动赋值注释掉,那么就只会调用拷贝构造和拷贝赋值了。原因如C++ primer中所说:
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
按这个说法,如果把类A中所有拷贝控制成员都注释掉,编译器会为我们合成移动构造函数或移动赋值运算符。