C++新标准之前对象的拷贝控制由拷贝构造函数,重载的拷贝赋值运算符,析构函数三个函数决定。
新标准之后新增两个函数:移动构造函数,移动赋值运算符
左值(lvalue) 指持久存在的对象或返回值类型为左值引用的返回值,是不可移动的。
右值(rvalue) 包含了临时对象或者返回值为右值引用的返回值,是可移动的。
为了支持移动操作,C++新标准引入了一种新的引用类型——右值引用&&
,即必须绑定到右值的引用
int &&i = 42; //正确 int j = 42; int &&k = j; //错误,右值引用不能绑定到左值 const int &r = j*42; //正确,const会创建一个临时的const变量 int &r1 = j*42; //错误,右值引用不能绑定到左值 int &&r2 = j*42; //正确,j*42是一个右值 int &&r3 = i; //错误,表达式i是一个左值
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符都是返回左值表达式的例子。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值,不能将左值引用绑定到这类表达式,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
有时候我们需要将左值像右值一样转移所有权
void func(){ A res; //..... if(xxx) ans = res; //我希望将res转移到外部变量ans上 return; }
在上述代码中,res赋值给ans之后不再被使用,我们希望调用的是移动赋值构造函数。
但是res是一个左值,因此ans = res调用的是赋值构造函数。
为了将某些左值当成右值使用,C++新标准提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。
void func(){ A res; //..... if(xxx) ans = std::move(res); //这时候调用的是移动赋值构造函数 return; }
void func1(A a) {return;} void func2(A &&a) {return;} int main() { A a; A &b = a; A c; A d; //请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销? func1(a); //调用拷贝构造函数 func1(b); //调用拷贝构造函数 func1(std::move(c)); //调用移动构造函数 func2(std::move(d)); //都不调用 }
实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。
实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。
倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。可以说,这种情况下,只提供非引用类型的版本,也是可以接受的。
从极致的优化角度来看,如果参数有支持移动构造(或移动赋值)的类型,应该同时提供左值引用(匹配左值)和右值引用(匹配右值)两种重载版本。
A func1() { A a; return a; } A func2() { A a; return std::move(a); } A &&func3() { A a; return std::move(a); } int main() { A test1 = func1(); //test1调用情况 Construct func A test2 = func2(); //test2调用情况 Construct func Move func Destroy func A test3 = func3(); //test3调用情况 Construct func Destroy func Move func return 0; }
执行这3行代码实际上都没有任何Copy Construct的开销,并且func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。
因此,不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,准确来说一般用于表示返还成一个右值,如std::move的实现)。
结论:
1. 我们应该首先把编写右值引用类型相关的任务重点放在对象的构造、赋值函数上。从源头上出发,在编写其它代码时会自然而然享受到了移动构造、移动赋值的优化效果。
2. 形参:从优化的角度上看,若参数有支持移动构造(或移动赋值)的类型,应提供左值引用和右值引用的重载版本。移动开销很低时,只提供一个非引用类型的版本也是可以接受的。
3. 返还值:不要且没必要编写返还右值引用类型的函数,除非有特殊用途。
参考:
1. <<C++ Primer>>第五版
2. 透彻理解C++11 移动语义:右值、右值引用std::move、std::forward