左值 右值 左值引用 右值引用 std::move std::forward
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 &&。考虑上面的例子,
1. `forward_value(a); => std::forward(val)); => return static_cast
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步