左值(lvalue)是指表达式结束后依然存在的持久对象,右值(rvalue)是指表达式结束时就不再存在的临时对象。一个区分左值和右值的便捷方法是: 看能不能对表达式取地址(即是否占用存储单元),如果能,则为左值,否则为右值。
C++11 增加了一个新的类型,称为右值引用(R-value reference),标记为 T &&
。
C++11 的右值概念由两部分构成,一个是将亡值(xvalue, expiring value),另一个则是纯右值(prvalue, PureRvalue)。
非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等都是纯右值。
将亡值是 C++11 为了引入右值引用而提出的概念,也就是即将被销毁、却能够被移动的值。它包括将要被移动的对象、T&&函数返回值、std::move 返回值和转换为 T&& 类型的转换函数的返回值等。
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名,所以像左值引用一样,声明右值引用时也必须进行初始化。
通过右值引用的声明,该右值才"重获新生",其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
实际上 T&& 未必一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。
template <typename T>
void f(T&& param);
f(10); // 10 是右值
int x = 10;
f(x); // x 是左值
像上面这样,当发生自动类型推断时,这个未定的引用类型称为 universal references 。它必须被初始化,它是左值还是右值引用取决于它的初始化,如果 && 被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。
编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
示例一:
int&& var1 = x;
auto&& var2 = var1;
var1 的类型是 int&&, 它是一个右值引用。var2 存在类型推断,因此是一个 universal references, auto&& 最终被推导为 int&。
示例二:
void PrintValue(int& i)
{
std::cout << "lvalue: " << i << std::endl;
}
void PrintValue(int&& i)
{
std::cout << "rvalue: " << i << std::endl;
}
void Forward(int&& i)
{
PrintValue(i); // 已命名的右值引用被视为左值
}
// main
int i = 0;
PrintValue(i);
PrintValue(1);
Forward(2);
输出结果如下:
lvalue: 0
rvalue: 1
lvalue: 2
Forward 函数接收的是一个右值,但在转发给 PrintValue 时又变成了一个左值,因为在 Forward 中调用 PrintValue 时,右值 i 变成了一个命名的对象,编译器会将其当作左值处理。
利用一个左值去初始化一个右值引用类型是非法的,编译会报错。这时如果希望把一个左值赋给一个右值引用类型,可以通过 std::move 解决。
int w;
int &rrw1 = w; // error
int &rrw2 = std::move(w); // ok
std::move 可以将一个左值转换成右值。
使用右值引用可以避免无谓的复制,近而提高程序性能。
对于如下测试程序:
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
A() { std::cout << "coustruct: " << ++g_constructCount << std::endl; }
A(const A& a) { std::cout << "copy construct: " << ++g_copyConstructCount << std::endl; }
~A() { std::cout << "destruct: " << ++g_destructCount << std::endl; }
};
A GetA() { return A(); }
// main
A a = GetA();
-
如果开启返回值优化,输出结果将是:
construct: 1 destruct: 1
编译器将会把临时对象优化掉,但这不是 C++ 标准,是各编译器的优化规则。
-
在 GCC 下编译时设置编译选项
-fno-elide-constructors
来关闭返回值优化。输出结果:construct: 1 copy construct: 1 destruct: 1 copy construct: 2 destruct: 2 destruct: 3
在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是 GetA() 函数内部创建的对象返回后构造一个临时对象产生的,另一次是在 main 函数中构造 a 对象产生的。多了两次拷贝,也就多了两次 destruct。
-
优化一: 通过右值引用来绑定 main 函数中返回值:
A&& a = GetA();
在编译时设置编译选项
-fno-elide-constructors
, 输出结果如下:construct: 1 copy construct: 1 destruct: 1 destruct: 2
通过右值引用,比之前少了一次拷贝和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。
-
优化二: 紧跟随优化一,通过右值引用来绑定 GetA() 函数中的返回值:
A&& GetA() { return A(); }
在编译时会有如下 warning 提示:
In function ‘A&& GetA()’: warning: returning reference to temporary [-Wreturn-local-addr] A&& GetA() { return A(); } ^
通过 std::move 设置移动:
A&& GetA() { return std::move(A()); }
在编译时设置编译选项 -fno-elide-constructors, 输出结果如下:
construct: 1 destruct: 1
可以看出,合理的使用右值引用,可以避免无谓的复制,进而提高程序的性能。
对于含有堆内存的类,如果使用默认构造函数,可能会导致堆内存的重复删除。
class A
{
public:
A() : m_ptr(new int(0)) { std::cout << "construct" << std::endl; }
~A() { std::cout << "destruct" << std::endl; delete m_ptr; }
private:
int* m_ptr;
};
A GetA() { return A(); }
// main
A a = GetA();
上述代码在开启编译器优化后,执行没有问题。但在关闭优化后(在编译时设置编译选项 -fno-elide-constructors
)再执行,会有 double free 错误。可以看出,单纯依靠编译器,会存在不安全因素。
上述不安全因素可以通过深拷贝来解决,但有时深拷贝构造却是不必要的,如上述代码的情形。且如果堆内存还在,还会带来额外的性能损耗。可以对类 A 进行下面的优化:
class A
{
public:
A() : m_ptr(new int(0)) { std::cout << "construct" << std::endl; }
A(const A& a) :m_ptr(new int(*a.m_ptr)) { std::cout << "copy(deep) construct" << std::endl; }
A(A&& a) :m_ptr(a.m_ptr) { a.m_ptr = nullptr; std::cout << "move construct" << std::endl; }
~A() { std::cout << "destruct" << std::endl; delete m_ptr; }
private:
int* m_ptr;
};
关闭优化后不再报 double free 错误,输出结果如下:
construct
move construct
destruct
move construct
destruct
destruct
程序在执行时会根据传入参数是左值还是右值来选择相应构造函数。如果是临时值,则会选择移动构造函数。 return A() 的返回值和 GetA() 函数的返回值都是临时值,所以选择了移动构造函数。
继续优化: 当然,即便选择了移动构造,但还是调用了两次移动构造,两次析构。继续优化如下:
A&& GetA() { return std::move(A(); }
// main
A&& a = GetA();
关闭优化后输出结果如下:
construct
destruct
可以看出,通过右值引用,可以对最开始创建的对象一用到底,中间连拷贝都"节省"了。但实际工程中并不会优化到这里,因为 a 是一个右值(将亡值),没有自己的地址,后续也就无法对其进行操作。
需要注意的是,在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。