有了左值引用为什么还需要右值引用?

在平时编码过程为了减少数据的拷贝,提高性能,我们一般通过引用的方式来传递参数,例如:

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(*)) {
cout << "拷贝构造函数" << endl;
}

A(A &&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中因为右值引用的出现,对值的类型进行了更具体的划分,

C++之右值引用_右值引用

在上图中,左值和纯右值我们比较好理解,左值正如上面所说的哪样,一般有内存地址的,存活的生命周期较长的,就是左值,而像整型1,浮点型1.1这种的无法通过取地址符号获取到

具体的内存地址的一般就是右值。令人眼花的是这个将亡值,它既可以代表一个左值,又可以代表一个右值,这是怎么的一回事呢?

万能引用和折叠引用

所谓的万能引用就是既可以引用左值,也可以引用右值的引用。例如:

// 右值引用,有明确的类型
}

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++之右值引用_右值_02

从图中可以看出在推导过程中,只有实际类型是一个非引用类型或者右值引用类型时,最后推导出来的才是一个右值引用,其余的推导结果都是左值引用。

完美转发

上面介绍了万能引用,它的一个重要用途就是进行完美转发,所谓完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数,不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

在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函数,

这就是完美转发的威力所在。

关注我,一起进步,人生不止coding!!!