一、结论
C
中将数据类型分为左值和右值,在C++11
中又将右值概念更为细致的分为将亡值(xvalue)和纯右值(prvalue)。- 将亡值是
C++11
新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用T&&
的函数返回值、std::move
的返回值。 - 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过
std::move
指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。 - 作为函数形参时,右值引用更灵活。虽然 const 左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
- 可移动对象在需要拷贝且被拷贝者之后不再被需要的场景,建议使用
std::move
触发移动语义,提升性能。 - 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和
std::move
的语言特性。 std::move
本身只做类型转换,对性能无影响。std::forward
同样也是做类型转换且更强大,move
只能转出来右值,forward
既可以转成右值,又可以转成左值。- 函数最好不要返回函数体内局部变量的左值引用或右值引用。
二、注意事项
- 左值引用是具名变量值的别名
- 右值引用是不具名(匿名)变量的别名
-
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,但是,const左值引用是可以指向右值的
const int &ref_a = 5; int a = 5; int &ref_a = a; // 左值引用指向左值,编译通过 int &ref_a = 5; // 左值引用指向了右值,会编译失败
-
右值引用的标志是
&&
,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值int&& ref_a_right = 5; // ok int a = 5; int&& ref_a_left = a; // 编译不过,右值引用不可以指向左值 ref_a_right = 6; // 右值引用的用途:可以修改右值
-
事实上
std::move
移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)
。 所以,单纯的std::move(xxx)
不会有性能提升,从这个角度来讲,右值引用和左值引用的功能相似,都是原始变量的别名,至于移动后原始变量是否可用,取决于被移动的数据类型对于移动语义的具体实现。案例一:
int a = 5; // a是个左值 int &ref_a_left = a; // 左值引用指向左值 int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向 cout << a << endl; // 打印结果:5 ref_a_right = 10; cout << a << endl; // 打印结果:10 ref_a_left = 20; cout << a << endl; // 打印结果:20 // 这三个变量的地址都相同
案例二:
class Person{ public: int m_age{20}; }; void test(Person&& p){ cout << __func__ << " " << &p << endl; } void test1(Person& p){ cout << __func__ << " " << &p << endl; } int main() { Person pp; cout << __func__ << " " << &pp << endl; test(move(pp)); cout << pp.m_age << endl; test1(pp); return 0; } 输出: main 0x7ffc06f34444 test 0x7ffc06f34444 20 test1 0x7ffc06f34444
-
被声明出来的左、右值引用都是左值。因为被声明出的左右值引用是有地址的,也位于等号左边。
// 形参是个右值引用 void change(int&& right_value) { right_value = 8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印这三个左值的地址,都是一样的 // error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’ // change(a); // 编译不过,a是左值,change参数要求右值 // change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 // change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 change(std::move(a)); // 编译通过 change(std::move(ref_a_right)); // 编译通过 change(std::move(ref_a_left)); // 编译通过 change(5); // 当然可以直接接右值,编译通过 }
-
右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
三、std::forward
std::forward<T>(u)
有两个参数:T
与 u
- a. 当
T
显示为左值引用类型时,u
将被转换为T
类型的左值; - b. 否则
u
将被转换为T
类型右值
void change2(int&& ref_r) {
ref_r = 1;
}
void change3(int& ref_l) {
ref_l = 1;
}
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
// error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
// change2(ref_r); // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
change2(std::forward<int&&>(ref_r)); // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
change2(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
change3(std::forward<int&>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
// 可见,forward可以把值转换为左值或者右值
}
int main() {
int a = 5;
change(std::move(a));
}
四、自定义类支持移动语义
编译器会默认在用户自定义的 class
和 struct
中生成移动语义函数,但前提是符合用户类默认的移动构造/移动赋值的启用条件,系统默认的移动语义函数更像是一种引用(原始变量的别名)。
if (定义了 拷贝构造函数 、 拷贝赋值运算符 或 析构函数) {
不会合成移动构造函数和移动赋值运算符。此时会使用对应 拷贝 操作来代替 移动
}
else if (类的所有成员都可以移动) {
则会为类合成移动构造函数或移动赋值运算符。
}
else {
合成拷贝构造函数和拷贝复制运算符。
}
C++11 对于特殊成员函数处理的规则如下:
- 默认构造函数:和 C++98 规则相同。仅当类不存在用户声明的构造函数时才自动生成。
- 析构函数:基本上和 C++98 相同;稍微不同的是现在析构默认
noexcept
。和 C++98 一样,仅当基类析构为虚函数时该类析构才为虚函数。 - 拷贝构造函数:和 C++98 运行时行为一样:逐成员拷贝
non-static
数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是delete
的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。 - 拷贝赋值运算符:和 C++98 运行时行为一样:逐成员拷贝赋值
non-static
数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是delete
的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。 - 移动构造函数和移动赋值运算符:都对非
static
数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
class Person{
public:
Person():m_age(new int(20)){
cout << "Construct: " << ++n_cstr << endl;
}
~Person(){
delete m_age;
cout << "Destruct: " << ++n_dstr << endl;
}
Person(const Person& p):m_age(new int(*p.m_age)){
cout << "Copy construct: " << ++n_cptr << endl;
}
Person(Person&& p){
m_age = std::exchange(p.m_age, nullptr);
cout << "Move construct: " << ++n_mvtr << endl;
}
Person& operator=(const Person& p){
*m_age = *p.m_age;
cout << "operator=(const Person& p)..." << endl;
return *this;
}
Person& operator=(Person&& p){
if(m_age) delete m_age;
m_age = std::exchange(p.m_age, nullptr);
cout << "operator=(Person&& p)..." << endl;
return *this;
}
int* m_age{nullptr};
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int Person::n_cstr = 0;
int Person::n_dstr = 0;
int Person::n_cptr = 0;
int Person::n_mvtr = 0;
Person GetPerson(){
Person p;
cout << *p.m_age << endl;
cout << "Resource from " << __func__ << ": " << p.m_age << endl;
return p;
}
int main(int argc, const char* argv[]){
Person p = GetPerson();
cout << *p.m_age << endl;
cout << "Resource from " << __func__ << ": " << p.m_age << endl;
return 0;
}
输出:
Construct: 1
20
Resource from GetPerson: 000001D5C8D85990
Move construct: 1
Destruct: 1
20
Resource from main: 000001D5C8D85990
Destruct: 2
参考: