C++11之右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的 右值引用(rvalue reference)语法特性,所以从现在开始我们之前学习的引用就叫做左值引用(lvalue reference)。无论左值引用还是右值引用,都是给对象取别名。
1. 左值和右值
C++的表达式要不然是右值(rvalue,读作“are-value”),要不然就是左值 (lvalue,读作“ell-value”)。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。但在C++语言中,二者的区别就没那么简单了。
- 左值:能对表达式取地址、或具名对象/变量。(如变量名或解引用的指针)
- 右值:不能对表达式取地址,或匿名对象。(如:字面常量、表达式返回值,函数返回值)
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
2. 左值持久;右值短暂
考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于右值引用只能绑定到临时对象,我们得知:
- 所引用的对象将要被销毁
- 该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象 “窃取” 状态。
3. 变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上:
int &&rr1 = 42;//正确:字面常量是右值
int &&rr2 = rr1;//错误:表达式rr1是左值!
其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
4. 移动语义
4.1 移动构造
在模拟实现的string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 移动构造
Janonez::string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
Janonez::string to_string(int value)
{
Janonez::string str;
// ...
return str;
}
int main()
{
Janonez::string ret2 = Janonez::to_string(-1234);
return 0;
}
再运行上面to_string的调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
4.2 移动赋值
不仅仅有移动构造,还有移动赋值:在Janonez::string类中增加移动赋值函数,再去调用Janonez::to_string(1234),不过这次是将
Janonez::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。
// 移动赋值
Janonez::string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
Janonez::string ret1;
ret1 = Janonez::to_string(1234);
return 0;
}
4.3 标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为 move 的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件<utility>
中。move函数返回给定对象的右值引用。
int &&rr3 = std::move(rr1); // ok
move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。调用 move 就意味:除了对 rr1 赋值或销毁它外,我们将不再使用它。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
5. 万能引用(T&&)和引用折叠
-
T&&的两种含义
- 右值引用:当T是确定的类型时,T&&为右值引用。
- 当T存在类型推导时,T&&为万能引用,表示一个未定的引用类型。如果被右值初始化,则T&&为右值引用。如果被左值初始化,则T&&为左值引用。
-
引用折叠
-
由于引用本身不是一个对象,C++标准不允许直接定义引用的引用。
-
当类型推导时可能会间接地创建引用的引用,此时必须进行引用折叠。具体折叠规则如下:
(1)凡是有左值引用参与的情况下,最终的类型都会变成左值引用。
A& &
、A& &&
和A&& &
都折叠成类型A&
。(2)只有全部为右值引用的情况才会折叠为右值引用。类型
A&& &&
折叠成A&&
。
-
6. 完美转发
看下面示例代码,按我们的理解传入不同属性的对象,会调用不同的fun函数,但实际却并不是这样。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行结果:
我们发现打印结果全都是左值,这和我们预期是不同的,这是因为对象在传递过程中会将它的右值属性转换为左值属性,这样才能转移资源,那么我们想让对象在传递过程中保持它的左值或者右值的属性, 就需要用到完美转发。std::forward
完美转发在传参的过程中保留对象原生类型属性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行结果: