本文是对《深入理解C++11》和《深入应用C++11:代码优化与工程级应用》中右值引用部分的总结。
在C++11中增加了新的类型---右值引用(T&&)。通过它可以实现移动语义和完美转发。
作用:
- 主要是避免无谓的复制,节省运算存储资源,提高程序性能。
- 能够更简洁明确地定义泛型函数。
概念:
- 左值:
能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
- 右值:
不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
右值中有两个重要概念:将亡值和纯右值。
1. 将亡值:C++11新增概念,是指具有转移语义的对象,比如:将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值。
2. 纯右值:是用于识别临时变量和一些不跟对象关联的值。如: 非引用返回的临时变量、运算表达式产生的临时变量、原始字面变量和Lambda表达式等。
右值引用就是对右值进行引用的类型。
T && a = ReturnRval();
因为右值不具名,所以只能通过引用方式找到它。作为一个引用类型,在声明时必须立即初始化,通过右值引用的声明,被引用对象生命周期与右值引用类型变量的生命周期一样,可以说右值引用能用来延长右值的生命周期。
虽然上述用T&& 来定义右值,但它并不一定只表示右值,它与其绑定的类型相关。
template
void f(T&& param);
f(10); // 10是右值
int x = 10;
f(x); // x是左值
由上面例子可以看出:param可能是左值,也可能是右值,这完全取决于在调用函数时所传递的参数类型。在加之有&&修饰所以param也被称作未定义的引用类型(universal references)。
param必须要被初始化,当由右值初始化时它便是右值,当被左值初始化时它便是左值。
所以只有&&发生自动类型推断时(函数模板的类型自动推导、auto关键字),&&才是一个未定的引用类型。
总结
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值,左值引用只能接受左值,但常量左值引用(const T& )也称作万能引用类型,它可以接受左值、右值、常量左值、常量右值。
- auto&& 或 函数自动类型推导的T&& 是一个未定的引用类型,它通过初始化来确定具体的引用类型。
- 引用折叠规则:右值引用叠加到右值引用上仍是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它变成左值引用,输入右值则变为具名的右值引用。
- 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
概念:
是将资源通过浅拷贝方式从一个对象转移到另一个对象。通过此方式可以减少不必要的临时对象的创建、拷贝、销毁,从而提高程序性能。
如下实现一个MyString类:
class MyString{
private:
char* m_data;
size_t m_len;
void copy_data(const char* s){
m_data = new char [m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString(){
m_data = NULL;
m_len = 0;
}
MyString(const char* p){
m_len = strlen(p);
copy_data(p);
}
MyString(const MyString& str){
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
}
MyString& operator=(const MyString& str){
if (this != &str){
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
return *this;
}
virtual ~MyString(){
if (m_data) free(m_data);
}
};
void test(){
MyString a;
a = MyString("Hello");
}
在MyString("Hello")对a进行初始化时,开始先构造一个临时对象,返回这个临时对象,再通过拷贝赋值函数构造a对象,调用结束后对临时对象进行销毁。
而通过右值引用方式则可以省去多余的构造和销毁。
右值引用代码:
MyString(MyString&& str){
std::cout << "Move Construct is called! source: " << str.m_data << std::endl;
m_len = str.m_len;
m_data = str.m_data; // 避免了不必要的拷贝
// 资源转移后临时对象不在指向该资源
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str){
std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
if(this != &str){
m_len = str.m_len;
m_data = str.m_data; // 避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
通过此方式使得临时对象的资源转交给目标对象,而不再是拷贝、销毁,这样既能节省资源,又能省去资源申请和释放的时间。尤其当临时对象申请较多资源时,此方式能大幅度提升程序性能。
深度拷贝与移动语义区别如下图:
所以在设计和实现类时,如果涉及动态申请大量的资源的类,应该要考虑右值引用的拷贝构造和拷贝赋值,以提高效率。另外需要注意的是除了提供上述函数外,通过也要提供常量左值引用的拷贝构造函数,以保证移动不成还可以拷贝构造。
首先运行下示程序:
template
void PrintT(T& t){
cout << "lvalue" << endl;
}
template
void PrintT(T&& t){
cout << "rvalue" << endl;
}
template
void TestForward(T&& v){
PrintT(v);
}
void main(){
TestForward(1);
int x = 1;
TestForward(x);
}
运行结果:
x是左值所以很好理解输出结果为lvalue。但1作为右值输出为lvalue是为什么?
因为1是右值,所以未定义的引用类型T&& v被一个右值初始化后变为具名的右值引用,所以在TestForWard函数内调用PrintT(v)时,此时v是具名变量,它是左值,所以被PrintT(T& )所调用,从而结果打印lvalue。(参照之前在右值引用处总结的三、四条)
那么我们想要保证参数按照原来类型转发到其他函数,就上例而言,希望v依然作为右值被转发。这种转发被称为完美转发。
实现就是靠std::forward函数实现:
template
void TestForward(T&& v){
//PrintT(v);
PrintT(std::forward(v)); // forward实现完美转发
}
运行结果: