总结左右值的定义,和 C++11 中右值引用的用途
左值与右值
C++ 中,左右值的简化定义:
- 左值:占用了一定内存,且拥有可辨认的地址的对象
- 右值:左值以外的所有对象
典型的左值,C++中绝大部分的变量都是左值
1 2 3 4 5 6
| int i = 1; int *p = &i; i = 2;
class A; A a;
|
典型的右值
1 2 3 4 5 6 7 8 9 10
| int i = 2; int x = i + 2; int *p = &(i + 2); i + 2 = 4;
class A; A a = A();
int sum(int a, int b) {return a + b;} int i = sum(1, 2)
|
引用(左值引用)
1 2 3 4 5 6
| int i; int &r = i; int &r = 5;
const int &r = 5;
|
对于函数,类似地有
1 2 3 4 5 6 7 8
| int square(int& a) {return a * a;}
int i = 2; square(i); square(2);
int square(const int& a) {return a * a;}
|
左值可以创造右值, 右值也可创造左值
1 2 3 4 5
| int i = 2; int x = i + 2;
int v[3]; *(v + 1) = 2;
|
一些注意事项
1 2 3 4 5 6 7 8 9 10 11 12 13
| int myglobal ; int& foo() {return myglobal;} foo() = 50;
array[3] = 50;
const int i = 1;
class dog; dog().bark();
|
右值引用
右值引用的两大用途:
移动语意
移动语意(std::move),可以将左值转化为右值引用
1 2 3 4 5 6 7 8 9 10 11 12 13
| int a = 1; int &b = a; int &&c = std::move(a);
void printInt(int& i) { cout << "lvalue reference: " << i << endl; } void printInt(int&& i) { cout << "rvalue reference: " << i << endl; }
int main() { int i = 1; printInt(i); printInt(6); printInt(std::move(i)); }
|
由于编译器调用时无法区分 printInt(int) 与 printInt(int&)
以及 printInt(int) 与 printInt(int&&)
,如果再定义 printInt(int) 函数,会报错
为什么需要移动语意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class myVector { int size; double* array; public: myVector(const myVector& rhs) { std::cout << "Copy Constructor\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) {} myVector createMyVector();
int main() { myVector reusable = createMyVector(); foo(reusable);
foo(createMyVector()); }
|
解决方法, 添加一个移动构造函数
1 2 3 4 5 6 7
| myVector(myVector&& rhs) { std::cout << "Move Constructor\n"; size = rhs.size; array = rhs.array; rhs.size = 0; rhs.array = nullptr; }
|
在 C++03 中,可能需要定义两个 foo 函数来解决这种问题:foo_by_value(myVector)
和 foo_by_ref(myVector&)
但是,假如我们在调用 foo 之后,reusable 就没用了, 可以使用移动语意
1 2 3 4 5 6
| int main () { myVector reusable = createMyVector(); foo(std::move(reusable)); }
|
另一个有用之处在于重载赋值运算符
1 2
| X& X::operator=(X const & rhs); X& X::operator=(X&& rhs);
|
自从 C++11,所有的 STL 都实现了移动构造
完美转发
完美转发是指
- 只有在需要的时候,才调用复制构造函数
- 左值被转发为左值,右值被转发为右值
参数转发
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void foo(myVector& v) {}
template<typename T> void relay(T arg) { foo(arg); }
int main() { myVector reusable = createMyVector(); relay(reusable);
relay(createMyVector()); }
|
这个实现有个问题,假如定义了两个版本的 foo
1 2
| void foo(myVector& v) {} void foo(myVector&& v) {}
|
永远只有 foo(myVector& v) 会被调用
所以,我们需要改写上文的 relay 函数,借助 std::forward
1 2 3 4
| 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,用于返回引用指向的类型
1 2 3 4
| template<class T> struct remove_reference;
remove_reference<int&>::type == int remove_reference<int>::type == int
|
于是,std::forword 的实现如下
1 2 3 4
| 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&&