C++之右值引用
有了左值引用为什么还需要右值引用?
在平时编码过程为了减少数据的拷贝,提高性能,我们一般通过引用的方式来传递参数,例如:
void func(const int &a){
}
int main() {
func(10); // 可以
int a = 20;
func(a); // 可以
return 0;
}
如果在上面的程序中我们将函数func
中的const
修饰去掉之后呢?我们发现调用func(10);
居然无法通过了,这是为什么呢?在C++中带const修饰的引用成为常量左值引用,常量左值引用是可以绑定右值的,如果去掉了const修饰的话,就不是常量左值引用了,就不能绑定右值了,func(10);
也就编译是失败了,因为在不带const修饰的函数func
中表明引用a是可以修改的,但是func(10);
传递进去的10确实不可修改的,这就产生了矛盾,也就无法通过编译了。
面对这种情况,右值引用的作用就发挥出来了。右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题,消除了诸如 std::vector、std::string 之类的额外开销。
什么是右值
那么什么是右值呢?按照我们以往的编程经验,一般认为位于等号左边的值就是左值,位于等号右边的值就是右值,然而真相并不是这样的。
int a = 10;
int b = a;
例如在异常的代码中如果认为位于表达式左边的值就是左值,位于表达式右边的值就是右值的话,那么在第一行代码中变量a是左值,在第二行代码中变量a却变成了右值, 显然这是矛盾的。那么到底该如何区分左值和右值呢?在C++ Primer中对左值和右值的归纳为:
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
大概的意思就是说左值就有内存地址的,存活的生命周期较长的,而右值一般是无法获取到内存地址的,生命周期是短暂的。还是以以上的代码为例子,
变量a和变量b都是可以通过取地址符号&
获取到具体的内存地址的,所以变量a和变量b都是左值,而10,是一个普通的字面量,是不可以通过取地址符号&
获取到具体的内存地址的,所以10就是右值。但是普通的字面量是右值,这有一个例外就是字符串字面量不是一个右值,比如"hello world",这个字符串,我们是可以通过取地址符号&
获取到地址的。
右值怎么用
在语法上右值就是在左值的基础上增加了一个符号&
,也就是使用&&
表示右值引用:
void func(int &a){
// 左值引用
}
void fuc(int &&a){
// 右值引用
}
int main() {
int && a = 10; // 右值引用
return 0;
}
右值引用的特点之一是可以延长右值的生命周期,比如以下程序:
int getX(){
return 10;
}
int main() {
int &a = getX();// 错误
const int &aa = getX();// 正确,常量左值引用
int && b = getX(); // 右值引用
return 0;
}
在上面的程序中我们通过右值引用b
延长了函数getX
返回值的生命周期。延长临时对象生命周期并不是这里右值引用的最终目标,其真实目标应该是减少对象复制,提升程序性能。
main.cpp
main.cpp
using namespace std;
class A {
public:
A() {
cout << "构造函数" << endl;
}
A(const A &a) {
cout << "拷贝构造函数" << endl;
}
~A() {
cout << "析构函数" << endl;
}
};
A getA() {
return A();
}
int main() {
A a1 = getA();
cout << "-----------------------------------" << endl;
A &&a2 = getA(); // 右值引用,在关闭RVO情况下减少了一次拷贝
return 0;
}
以上程序在C++11基础上关闭RVO优化,使用命令g++ main.cpp -std=c++11 -o main -fno-elide-constructors
进行编译后,执行main可执行文件发现如下输出:
构造函数
拷贝构造函数
析构函数
拷贝构造函数
析构函数
-----------------------------------
构造函数
拷贝构造函数
析构函数
析构函数
析构函数
通过对比发现确实使用右值引用比普通的引用减少了一次拷贝。到这里可能会有人说有了RVO优化就行了,还需要右值引用干嘛?其实这仅仅是举例说明右值引用的一个小小的场景而已,其用处远不止于此。
移动构造
对于拷贝构造函数而言形参是一个左值引用,而不能是某些函数返回的临时对象,而且在拷贝构造函数中往往进行的是深复制,即一般不会破坏实参对象。而移动构造函数恰恰相反,它接受的是一个右值,其核心思想是通过转移实参对象的数据以达成构造目标对象的目的, 也就是移动构造函数会修改实参对象,一般来说调用了移动构造函数之后,实参对象的相关变量资源就会被转移,原本实参的变量就会被置空,也就是实参就不能再使用了, 因此与其叫做移动构造函数不如叫做窃取构造函数更加的贴切。
那么在什么情况会发生移动构造的调用呢?比如在C++11的STL容器中,会根据具体情况自动调用移动构造函数,比如以下例子:
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
A(string s):name(new string(s)) {
cout << "构造函数" << endl;
}
A(const A &a):name(new string(*a.name)) {
cout << "拷贝构造函数" << endl;
}
A(A &&a){
name = a.name;
a.name = nullptr; // 置空
cout << "移动构造函数" << endl;
}
~A() {
cout << "析构函数" << endl;
delete name;
name = nullptr;
}
public:
string *name;
};
int main() {
vector<A> vector;
vector.push_back(A("world")); // 移动构造函数,如果有的话,如果没有则调用的是拷贝构造函数
return 0;
}
处理在STL容器中可能会自动调用移动构造函数外,我们也可以手动通过std::move
或者类型转换static_cast
手动地调用移动构造函数,例如针对以上的类A:
int main() {
A a("hello");
A b = std::move(a); // 如果没有移动构造函数则会自动调用拷贝构造函数
A c = static_cast<A&&>(b); // 如果没有移动构造函数则会自动调用拷贝构造函数
return 0;
}
注意:和拷贝构造函数对于拷贝赋值运算符一样,移动构造函数也对于这一个移动赋值运算符,因为在移动语义中一般会置空实参的相关变量,所以需要注意在移动赋值运算符避免自己赋值给自己的情况
左值、右值、将亡值
在C++11中因为右值引用的出现,对值的类型进行了更具体的划分,
万能引用和折叠引用
所谓的万能引用就是既可以引用左值,也可以引用右值的引用。例如:
// 右值引用,有明确的类型
}
void test(int &){
// 左值引用
}
template<typename T>
void test(T &&){
// 万能引用,因为模板需要类型推导
}
int getNum(){
return 20;
}
int main() {
int &&num1 = getNum(); // 右值引用
auto &&num2 = getNum(); // 万能引用,类型推导
return 0;
}
在上面的注释中我们发现只要发生了类型推导就会是万能引用,在T&&和auto&&的初始化过程中都会发生类型的推导所以它们是万能引用。在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会推导出左值引用;反之如果源对象是一个右值,则会推导出右值引用。
在C++11中有一套引用叠加推导的规则叫做引用折叠,通过这套规则,我们可以推导出万能引用的最终类型是什么,如下图:
从图中可以看出在推导过程中,只有实际类型是一个非引用类型或者右值引用类型时,最后推导出来的才是一个右值引用,其余的推导结果都是左值引用。
完美转发
上面介绍了万能引用,它的一个重要用途就是进行完美转发,所谓完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数,不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。在C++11使用标准库中的std::forward
函数就可以试下完美转发:
void test(int &t){
// 左值引用
cout << "左值" << endl;
}
void test(int &&t){
// 右引用
cout << "右值" << endl;
}
template<typename T>
void funcForward(T &&t){
// 进行了转发,根据传递进来的值类型而调用不同test
test(std::forward<T>(t));
}
template<typename T>
void funcNormal(T &&t){
// 没有进行转发,始终调用的都是左值的test
test(t);
}
int main() {
int a = 20;
funcNormal(1); // 右值,但是调用的是左值的test
funcNormal(a); // 左值
cout << "----------------------" << endl;
funcForward(1); // 右值
funcForward(a); // 左值
return 0;
}
输出:
左值
左值
----------------------
右值
左值
在上面的例子中我们发现,函数funcNormal
无论传递进去的左值还是右值最终调用的都是左值的test函数,而函数funcForward
会根据传递的的实参类型是左值还是右值调用不同的test函数,
这就是完美转发的威力所在。
推荐阅读
《C++之指针扫盲》
《C++之智能指针》
《C++之指针与引用》
关注我,一起进步,人生不止coding!!!