1. 左值, 右值
能取地址的是左值, 否则右值
- int foo() {
- return 0;
- }
- int a = 1; //a 左值, &a 有效
- int &&b = 1; //b 左值, &b 有效
- a + 1; //a + 1 右值,&(a+1) 无效
- foo(); //foo() 右值, &(foo()) 无效
- 1; // 右值, &1 无效
- int c = b + a;//a, b, c 都是左值, 不要以出现在等号左右来区分左值右值
2. 左值引用, 常量左值引用, 具名右值引用, 无名右值引用
引用分为四种: 左值引用, 常量左值引用, 具名右值引用, 无名右值引用
左值引用 T &: 左值引用需要内存地址, 因此, 只能绑定左值.
常量左值引用 const T &: 常量左值引用可以绑定左值或者右值.
具名右值引用 T && : 带有变量名的右值引用, 只能绑定右值.
无名右值引用 std::move(T): 没有变量名的右值引用, 不能绑定左值或者右值.
是否可以绑定 | 左值 | 右值 |
---|---|---|
左值引用 | Yes | No |
常量左值引用 | Yes | Yes |
具名右值引用 | No | Yes |
无名右值引用 | No | No |
- int foo() {
- return 0;
- }
- int a = 0;
- int &b = a; // 可以, 左值引用绑定左值
- int &c = 1; // 不行, 左值引用不能绑定右值
- int &d = foo(); // 不行, 左值引用不能绑定右值
- const int &e = a; // 可以, 常量左值引用可以绑定左值
- const int &f = foo(); // 可以, 常量左值引用可以绑定右值
- int && g = b; // 不可以, 具名右值引用不可以绑定左值
- int && h = foo(); // 可以, 具名右值引用绑定右值
- int && i = a + a; // 可以, 具名右值引用绑定右值
- std::move(a) = 1; // 不可以, 无名右值引用不能绑定左值或者右值
- int && j = std::move(a);// 可以, 具名右值引用绑定无名右值引用
左值引用, 左值常量引用, 和具名右值引用是 3 种变量类型, 和 int, char, long 一样都有地址, 因此都是左值. 而无名右值引用没有地址, 不属于左值.
在实现中, 和左值引用不一样, 具名右值引用会为自己的引用的内容开辟了一块内存空间. 此时一个具名右值引用所需要的空间为 内容地址 (x64: 8 字节) + 内容大小, 比如下面的例子, 在栈上开辟了 12 字节, 后 4 字节存放 (int)1, 前 8 字节存放内容的地址
- int main() {
- int &&a = 1;
- return 0;
- }
其汇编为
- .cfi_startproc
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- movl $1, %eax
- movl %eax, -12(%rbp) ; [rbp - 12 , rbp - 8) <-- (int)1
leaq -12(%rbp), %rax ; 取地址, rax = rbp - 12
movq %rax, -8(%rbp) ; 存地址 [rbp - 8, rbp) <--- rbp - 12
- movl $0, %eax
- popq %rbp
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
因此, 我们可以修改具名右值引用的内容, 而无需像左值引用一样必须先申请一个变量, 下例输出 2
- #include <iostream>
- int foo() {
- return 0;
- }
- int main() {
- int &&a = foo();
- a = 2;
- std::cout<<a<<std::endl;
- }
所以在具名右值引用的操作下, 我们可以修改右值, 可以通过具名右值引用本身直接修改, 也可以通过 T & = T && 的方式让其他引用修改
3. std::move
std::move 负责将左值或者右值变为无名右值引用. 首先看一下 move 的实现:
- template <typename T>
- struct remove_reference { typedef T type; };
- template <typename T>
- struct remove_reference<T&> { typedef T type; };
- template <typename T>
- struct remove_reference<T&&> { typedef T type; };
- template<typename T>
- typename std::remove_reference<T>::type&& move(T&& t) {
- return static_cast<typename std::remove_reference<T>::type&&>(t);
- }
其中 std::remove_reference 负责去掉类型中的引用, 比如 将 int & 变为 int
可以看到, move 输入是一个具名右值引用, 而输出是一个无名右值引用. 输入输出有什么不同呢? 输入的 T 本身也可能是非引用比如 int, std::string, 也可能引用比如 std::string &, int &&, 而输出只能是无名右值引用. 为了搞清楚这一变化, 先用以下代码用以检测输入 T 到底是什么类型
- #include <iostream>
- #include <string>
- template<typename T>
- bool T_is_value(T && x) {
- using U = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && (!std::is_const<U>::value);
- }
- template<typename T>
- bool T_is_const_value(T && x) {
- using U = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && std::is_const<U>::value;
- }
- template<typename T>
- bool T_is_left_ref(T && x) {
- using U = typename std::remove_reference<T>::type&;
- using V = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && (!std::is_const<V>::value);
- }
- template<typename T>
- bool T_is_const_left_ref(T && x) {
- using U = typename std::remove_reference<T>::type&;
- using V = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && std::is_const<V>::value;
- }
- template<typename T>
- bool T_is_right_ref(T && x) {
- using U = typename std::remove_reference<T>::type&&;
- using V = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && (!std::is_const<V>::value);
- }
- template<typename T>
- bool T_is_const_right_ref(T && x) {
- using U = typename std::remove_reference<T>::type&&;
- using V = typename std::remove_reference<T>::type;
- return std::is_same<T, U>::value && std::is_const<V>::value;
- }
- class A {};
- int main () {
- const A a;
- std::cout<<"T_is_value"<<T_is_value(a)<<std::endl;
- std::cout<<"T_is_const_value"<<T_is_const_value(a)<<std::endl;
- std::cout<<"T_is_left_ref"<<T_is_left_ref(a)<<std::endl;
- std::cout<<"T_is_left_const_ref"<<T_is_const_left_ref(a)<<std::endl;
- std::cout<<"T_is_right_ref"<<T_is_right_ref(a)<<std::endl;
- std::cout<<"T_is_const_right_ref"<<T_is_const_right_ref(a)<<std::endl;
- return 0;
- }
输出
- T_is_value 0
- T_is_const_value 0
- T_is_left_ref 0
- T_is_left_const_ref 1
- T_is_right_ref 0
- T_is_const_right_ref 0
因此 T 是一个常量左值引用类型 T = const A &. 由于子函数入口参数类型都是具名右值引用 T&&, 因此实际输入的实参是 const A & &&, 编译器是怎么得到这一类型的? 首先, C++ 有如下 reference collapse 标准:
- T && & = T &
- T& && = T &
- T && && = T &&
T 可以带有 const 或是 volatile
来看这里的例子:
a 类型是 const A , 如果 T = const A, 那么参数传递为: const A && t = a, 这是不允许的, 因为 a 是左值, 不能绑定到具名右值引用.
如果 T = const A &, 那么参数传递为 const A & && t = a, 根据之前的 reference collapse 规则, 有 const A & t = a, 常量左值引用绑定左值这是可以的.
如果 T = const A &&, 则由 collapse 规则得到 const A && t = a, 具名右值引用绑定左值, 是不可以的.
因此编译器采用第二种方案
如果将上述 main 函数里的 a 改成左值引用: A b; const A & a = b, 则 T = const A &
如果将上述 main 函数里的 a 改成 std::move(a), 则 T = const A
注意具名右值引用保留 const/volatile 这一特点, 当一个函数有 n 个参数, 每个参数都需要重载 const / non-const 引用的时候, 我们可以用 T && 来统一这两种类型
4 std::forward
从上面的例子我们看出, 由于函数参数总是左值, 因此经过一次函数调用后, 无名右值引用就消失了. 考虑如下代码
- #include <iostream>
- template <typename T>
- void print(T &t) {
- std::cout<<"T &t"<<std::endl;
- }
- template <typename T>
- void print(const T &t) {
- std::cout<<"const T &t"<<std::endl;
- }
- template <typename T>
- void print(T &&t) {
- std::cout<<"T &&t"<<std::endl;
- }
- template <typename T>
- void forward_value(T&& val) {
- print(val);
- }
- class A {};
- int main () {
- A a;
- forward_value(a);
- forward_value(std::move(a));
- return 0;
- }
输出
T &t T &t
这样函数 void print(T &&t) 就无法调用了, 如何改变这一点呢?
std::forward 解决了这个问题, 将代码作如下改变:
- template <typename T>
- void forward_value(T&& val) {
- print(std::forward<T>(val));
- }
则输出
T &t T &&t
std::forward 是如何做到这一点的? 看一下实现
- template<typename T>
- T&& forward(typename std::remove_reference<T>::type& t) {
- return static_cast<T&&>(t);
- }
- template<typename T>
- T&& forward(typename std::remove_reference<T>::type&& t) {
- static_assert(!std::is_lvalue_reference<T>::value, "template argument"
- "substituting T is an lvalue reference type");
- return static_cast<T&&>(t);
很显然了, 就是再次转 T &&. 考虑上面的例子,
- forward_value(a); => std::forward<A &>(val)); => return static_cast<T& &&>(t); => print(A &)
- forward_value(std::move(a)); => std::forward<A>(val)); => return static_cast<T&&>(t); => print(A &&)
来源: http://www.bubuko.com/infodetail-3303103.html