一、什么是右值引用
右值引用是 C++11 中引入的一种重要特性,它主要用于支持移动语义和完美转发。右值引用 是对右值(即临时对象或即将被销毁的对象)的引用,允许我们直接操作这些对象的资源,而无需进行拷贝。在 C++98 中,临时对象(右值)在赋值给函数参数时,只能被接受为 const 引用,这意味着函数无法修改这些对象的值。C++11 引入右值引用后,我们可以使用非常量引用(即右值引用)来接收这些临时对象,并在函数中直接操作它们。
左值和右值是表达式的分类,它们的主要区别在于表达式的值是否可以取地址。左值是可以取地址的表达式,而右值通常是不能取地址的表达式。
- 左值(Lvalue):通常有明确存储地址的表达式,如变量、对象的名称等。
- 右值(Rvalue):通常是没有明确存储地址的表达式,如字面量、临时对象、返回临时对象的表达式等。
右值引用是对右值的引用,它使用 &&
操作符来声明。右值引用的主要目的是为了实现移动语义,即允许资源的所有权从一个对象转移到另一个对象,从而避免不必要的拷贝,提高性能。
#include <iostream>using namespace std;int main(void)
{int num= 10; // 左值int &a = num; // 左值引用int && b = 30; // 右值引用const int &c = num; // 常量左值引用const int &&d = 50; // 常量右值引用// 左值引用可以使用右值引用初始化int &e = b;const int &f = b;const int &g = d; // 右值引用只能使用右值初始化return 0;
}
二、右值引用的作用
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,这是因为引用本身并不拥有所绑定对象的内存,它只是该对象的一个别名。通过右值引用的声明,该右值又 “重获新生”,其 生命周期与右值引用类型变量的生命周期一样,只要该变量还存活,该右值临时变量将会一直存活下去。
#include <iostream>using namespace std;class MyClass
{
private:int *p;public:MyClass(void); // 无参构造函数MyClass(const MyClass &other); // 拷贝构造函数~MyClass(void); // 析构函数
};MyClass::MyClass(void) : p(new int(100))
{cout << "Constructor called" << endl;
}MyClass::MyClass(const MyClass &other) : p(new int(*other.p))
{cout << "Copy constructor called" << endl;
}MyClass::~MyClass(void)
{cout << "Destructor called" << endl;delete p;
}MyClass getMyClass(void)
{MyClass temp;return temp; // 返回一个临时对象,这是一个右值
}int main(void)
{MyClass obj = getMyClass();return 0;
}
这里,我们要使用命令 gcc -o template.cpp -fbi-elide-constructors
来手动编译 C++ 程序,使用命令行参数 -fno-elide-constructors
用于关闭函数返回值优化(RVO)。这是因为 GCC 的 RVO 优化会减少复制构造函数的调用。
运行这段程序,会发现该程序发生三次构造。首先 getMyClass() 函数中 MyClass temp;
会调用 无参的构造函数,然后 return temp;
会使用 复制构造 产生临时对象,接着 MyClass obj = GetMyClass();
会使用 复制构造 将临时对象复制到 obj,最后临时对象被销毁。
但如果将 MyClass obj = getMyClass();
替换为 MyClass &&obj = getMyClass();
使用右值引用后,会调用两次构造函数,一次是 getMyClass() 中 MyClass temp
会调用 无参的构造函数,另一次是 return temp;
会使用 复制构造 产生临时对象。不同的是,由于 obj 是一个右值引用,引用的对象是函数 getMyClass() 返回的临时对象,因此该临时对象的生命周期得到延长,所以我们可以在 MyClass &&obj = getMyClass()
语句结束后继续调用该对象的其它方法而不会发生任何问题。
三、移动语义
上述代码中 3 次构造函数的调用,不难发现第二次和第三次的复制构造是影响性能的主要原因。在这个过程中都有临时对象参与进来,而临时对象本身只是做数据的复制。如果能将临时对象的内存直接转移到 obj 对象中,就能消除内存复制对性能的消耗。在 C++11 标准中引入了 移动语义,它可以帮助我们将临时对象的内存移动到 obj 对象中,以避免内存数据的复制。
#include <iostream>using namespace std;class MyClass
{
private:int *p;public:MyClass(void); // 无参构造函数MyClass(const MyClass &other); // 拷贝构造函数MyClass(MyClass &&other); // 移动构造函数~MyClass(void); // 析构函数
};MyClass::MyClass(void) : p(new int(100))
{cout << "Constructor called" << endl;
}MyClass::MyClass(const MyClass &other) : p(new int(*other.p))
{cout << "Copy constructor called" << endl;
}MyClass::MyClass(MyClass &&other) : p(other.p)
{cout << "Move constructor called" << endl;other.p = nullptr;
}MyClass::~MyClass(void)
{cout << "Destructor called" << endl;delete p;
}MyClass getMyClass(void)
{MyClass temp;return temp; // 返回一个临时对象,这是一个右值
}int main(void)
{MyClass obj1 = getMyClass();return 0;
}
在上面的代码中 MyClass 类中增加了构造函数 MyClass(MyClass&& other)
,它的形参是一个 右值引用 类型,称为 移动构造函数。
对于 复制构造函数 而言形参是一个 左值引用,也就是说函数的实参必须是一个具名的 左值,在复制构造函数中往往进行的是 深复制,即在不能破坏实参对象的前提下复制目标对象。而 移动构造函数 恰恰相反,它接受的是一个 右值,其核心思想是通过 转移实参对象的数据 以达成构造目标对象的目的,也就是说实参对象是会被修改的。
运行程序可以发现,后面两次的构造函数变成了 移动构造函数,因为这两次操作中源对象都是 右值(临时对象),对于右值编译器会优先选择使用移动构造函数去构造目标对象。当移动构造函数不存在的时候才会退而求其次地使用复制构造函数。在移动构造函数中使用了指针转移的方式构造目标对象,所以整个程序的运行效率得到大幅提升。
四、完美转发
完美转发(Perfect Forwarding)是 C++11 中引入的一个特性,它允许在函数模板中将参数连同其值类别(左值或右值)不变地转发给另一个函数。这意味着如果原始参数是一个左值,它将被转发为左值;如果原始参数是一个右值,它将被转发为右值。完美转发通常涉及到两个函数模板:std::forward
和 std::move
。std::forward
模板函数用于转发参数,而 std::move
用于将左值转换为右值引用。
#include <iostream>
#include <type_traits>using namespace std;
void printValue(int& value)
{cout << "Lvalue reference to: " << value << endl;
}void printValue(int&& value)
{cout << "Rvalue reference to: " << value << endl;
}template<typename T>
void forwardValue(T &&value)
{printValue(std::forward<T>(value));
}int main(void)
{int x = 42;forwardValue(x); // 转发左值forwardValue(42); // 转发右值forwardValue(std::move(x)); // 强制转为右值并转发return 0;
}
在这个示例中,forwardValue
是一个模板函数,它接受一个通用引用参数 T&&
。使用 std::forward<T>(value)
,我们可以将参数 value
连同其值类别完美地转发给 printValue()
函数。这样,printValue()
函数就能根据接收到的参数是左值还是右值来选择正确的重载版本。