本文总结 C++ 左右值的定义,和 C++11 中右值引用的用途。
转载:C++11新特性3 - 左右值与右值引用 C++ 11: Rvalue Reference – Move Sematics C++ 11: Rvalue Reference – Fowarding
C++11通过引入右值引用来优化性能,通过移动语义来减少无谓的拷贝,通过完美转发来解决不能按照参数实际类型来
要点:
什么是右值:字面值和临时值/将亡值称为右值,或者说不能取地址的值称为右值右值引用特点一:引用函数返回的将亡值,延长将亡值得生命周期,减少对象拷贝次数(const常量引用也能实现同样的效果,编译器返回值优化RVO能实现更好的效果(g++ -fno-elide-constructors关闭优化))右值引用特点二:既可引用右值,又可引用左值,右值引用特点三:T&& t是未定的引用类型(通用引用),是左值还是右值取决于它的初始化,可实现完美转发C++ 中,左右值的简化定义:
左值:占用了一定内存,且拥有可辨认的地址的对象右值:左值以外的所有对象 典型的左值,C++中绝大部分的变量都是左值 int i = 1; // i 是左值 int *p = &i; // i 的地址是可辨认的 i = 2; // i 在内存中的值可以改变 class A; A a; // 用户自定义类实例化的对象也是左值典型的右值
int i = 2; // 2 是右值 int x = i + 2; // (i + 2) 是右值 int *p = &(i + 2);//错误,不能对右值取地址 i + 2 = 4; // 错误,不能对右值赋值 class A; A a = A(); // A() 是右值 int sum(int a, int b) {return a + b;} int i = sum(1, 2) // sum(1, 2) 是右值引用(左值引用)
int i; int &r = i; int &r = 5; // 错误,不能左值引用绑定右值 // const 引用是例外 // 可以认为指向一个临时左值的引用,其值为5 const int &r = 5;对于函数,类似地有
int square(int& a) {return a * a;} int i = 2; square(i); // 正确 square(2); // 报错,左值引用绑定右值 // 变通一下 int square(const int& a) { return a * a; } // 可以调用 square(2)左值可以创造右值, 右值也可创造左值
int i = 2; int x = i + 2; // i + 2 是右值 int v[3]; // 指针解引用可以把右值转化为左值 *(v + 1) = 2;一些注意事项
// 1. 函数也可以返回左值 int myglobal ; int& foo() {return myglobal;} foo() = 50; // 一个常见的例子,[]操作符几乎总是返回左值 array[3] = 50; // 2. 左值并不总是可修改的 // i是左值, 但不可修改 const int i = 1; // 3. 右值有时是可修改的 class dog; // bark() 可能会修改 dog() 对象 dog().bark();右值引用的两大用途:
延长临时值生命周期移动构造,浅拷贝完美转发,区分左值右值以下代码调用了几次A的构造函数:
A getA() { return A(); } int main() { // 这句代码调用了几次A的构造函数(1次无参构造,2次拷贝构造) A a1 = getA(); // 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造) const A& a2 = getA(); // 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造) A& a3 = getA(); }右值引用能延长临时变量的声明周期,起到和常量左值引用类似的效果;但是常量左值引用是个常量,而右值引用不是常量,可以调用对象的const和非const函数。
类通常都会带有一个拷贝构造函数,但有时候我们又需要一个浅拷贝和一个深拷贝两个构造函数。尤其是拷贝构造的参数是个临时值时,我们通常希望只做浅拷贝,把成员变量的所有权移动至新对象,这就是所谓的移动语义,右值引用的一个重要作用就是用来支持移动语义。
如果从临时对象拷贝构造,希望调用浅拷贝;如果从右值对象拷贝构造,希望调用深拷贝:
class A { public: A() { } A(const A&a) // 拷贝构造 { size_t len = strlen(a->str); str = (char*)malloc(len+1); memset(str,0,len+1); memcpy(str,a->str,len); } A(A&&a) // 移动构造 { str = a->str; a->str = NULL; } ~A() { if(nullptr != str) { free(str); str = NULL; } } char * str =NULL; } A GetA() { return A(); } int main() { A a = GetA(); // 调用移动构造 A a2 = a; // 调用拷贝构造 }在上面的移动构造函数中,参数必须是右值引用,如果一个左值也想要使用移动构造函数创建新的对象,该怎么办?
移动语意(std::move),可以将左值转化为右值引用。查看源码,std::move等同于一个类型转换:static_cast<T&&>(lvalue)。
还是使用上面的代码测试:
int main() { A a = GetA(); // 调用移动构造 A a2 = a; // 调用拷贝构造 A a3 = std::move(a); // 调用移动构造,1. std::move(a) 返回a的右值引用;2. 以a的右值引用移动构造a3 }其他测试:
int a = 1; // 左值 int &b = a; // 左值引用 // 移动语意: 转换左值为右值引用 int &&c = std::move(a); void printInt(int& i) { cout << "lval ref: " << i << endl; } void printInt(int&& i) { cout << "rval ref: " << i << endl; } int main() { int i = 1; // 调用 printInt(int&), i是左值 printInt(i); // 调用 printInt(int&&), 6是右值 printInt(6); // 调用 printInt(int&&),移动语意 printInt(std::move(i)); }由于编译器调用时无法区分
printInt(int) 与 printInt(int&)printInt(int) 与 printInt(int&&)如果再定义 printInt(int) 函数,会报错
为什么需要移动语意
class myVector { int size; double* array; public: // 复制构造函数 myVector(const myVector& rhs) { std::cout << "Copy Construct\n"; size = rhs.size; array = new double[size]; for (int i=0; i<size; i++) { array[i] = rhs.array[i]; } } myVector(int n) { size = n; array = new double[n]; } }; void foo(myVector v) { /* Do something */ } // 假设有一个函数,返回值是一个 MyVector myVector createMyVector(); int main() { // Case 1: myVector reusable=createMyVector(); // 这里会调用 myVector 的复制构造函数 // 如果我们不希望 foo 随意修改 reusable // 这样做是 ok 的 foo(reusable); /* Do something with reusable */ // Case 2: // createMyVector 会返回一个临时的右值 // 传参过程中会调用拷贝构造函数 // 多余地被复制一次 // 虽然大部分情况下会被编译器优化掉 foo(createMyVector()); }解决方法, 添加一个移动构造函数
// 移动构造函数 myVector(myVector&& rhs) { std::cout << "Move Constructor\n"; size = rhs.size; array = rhs.array; rhs.size = 0; rhs.array = nullptr; }那么,foo(createMyVector())就不会调用拷贝构造函数,而会调用移动构造函数 (当然更大的可能性是编译器直接把这一步优化掉)
然而,在 C++03 中,为了解决这个问题,可能需要定义两个 foo 函数,比较麻烦:
foo_by_value(myVector)foo_by_ref(myVector&)同时,假如我们在调用 foo 之后,reusable 就不再被使用了, 可以使用移动语意
int main () { myVector reusable = createMyVector(); // 这里会调用 myVector 的移动构造函数 foo(std::move(reusable)); /* No use of reusable anymore */ }另一个有用之处在于重载赋值运算符
X& X::operator=(X const & rhs); X& X::operator=(X&& rhs);自从 C++11,所有的 STL 都实现了移动构造
在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。完美转发需要用到std::forward函数。
完美转发是指将右值引用实际引用的值类型进行转发,左值转发到左值引用的函数,右值转发到右值引用的函数。可以发现移动构造也是通过完美转发实现的。
左值被转发为左值,右值被转发为右值 void show_log(string& str) { cout << "string & show " << str.data() << endl; } void show_log(string &&str) { cout << "string && show " << str.data() << endl; } // 参数转发 template <typename T> void show(T&& arg) { // show_log(arg); // arg为具名变量,为左值,只有左值函数会被调用 show_log(std::forward<T>(arg)); // std::forward获取变量的原类型 } int main() { string msg = "hello"; show(msg); show(std::move(msg)); } void foo(myVector& v) {} // 参数转发 template<typename T> void relay(T arg) { foo(arg); } int main() { myVector reusable= reateMyVector(); // 拷贝构造函数 relay(reusable); // 移动构造函数 relay(createMyVector()); }这个实现有个问题,假如定义了两个版本的 foo
void foo(myVector& v) {} void foo(myVector&& v) {}永远只有 foo(myVector& v) 会被调用 (右值引用myVector&& v是个左值,这里的v是有名字的)
所以,我们需要改写上文的 relay 函数,借助 std::forward
template<typename T> void relay(T&& arg) { foo(std::forward<T>(arg)); }于是就有
relay(reusable) 调用 foo(myVector&)relay(createMyVector()) 调用 foo(myVector&&)要解释完美转发的原理,首先引入 C++11 的引用折叠原则
T& & => T&T& && => T&T&& & => T&T&& && => T&&所以,在 relay 函数中
relay(9); => T = int&& => T&& = int&& && = int&&relay(x); => T = int& => T&& = int& && = int &因此,在这种情况下,T&& 被称作 universal reference,即满足
T 是模板类型T 是被推导出来的,适用引用折叠,即 T 是一个函数模板类型,而不是类模板类型然后,C++11 定义了 remove_reference,用于返回引用指向的类型
template<class T> struct remove_reference; remove_reference<int&>::type == int remove_reference<int>::type == int于是,std::forword 的实现如下
template<class T> T&& forward( typename remove_reference<T>::type& arg ) { return static_cast<T&&>(arg); }等于是把右值引用(右值引用本身是个左值)转成了右值,左值保持不变
std::move 对比 std::forward:
std::move(arg) 将 arg 转化为右值std::forward(arg) 将 arg 转化为 T&&