左值、右值
左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。
很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:
看能不能对表达式取地址,如果能,则为左值,否则为右值。
int i = 0; // i是左值,0是右值
class Base
{
public:
int base;
};
Base getBase()
{
return Base();
}
Base b = getBase(); // b是左值,getBase()的返回值是右值(临时变量)
左值引用、右值引用
C++98中的引用很常见了,就是给变量取了个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(lvalue reference)。C++11中的右值引用使用的符号是&&。
右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用。
比如:T && a=ReturnRvalue()。假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
基于右值引用可以实现移动语义和完美转发新特性。
int a = 1;
int& b = a; //b是a的别名,a是左值。
int& c = 1; //编译错误! 1是右值,不能够赋值给左值引用
int&& d = 1; //实质上就是将不具名(匿名)变量取了个别名
int&& e = a; //编译错误!a是左值,不能将一个左值赋值给一个右值引用
class Base
{
public:
int base;
};
Base getBase()
{
return Base();
}
Base&& base = getBase(); // getBase()的返回值是右值(临时变量)
getBase返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量base的生命期一样,只要base还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。
注意:这里base的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。
总结一下,其中T是一个具体类型:
- 左值引用,使用 T&,只能绑定左值
- 右值引用,使用 T&&,只能绑定右值
- 常量左值,使用 const T&,既可以绑定左值又可以绑定右值
- 已命名的右值引用,编译器会认为是个左值
- 编译器有返回值优化,但不要过于依赖
移动语义
class Base
{
public:
Base(): data(new int(0)) { }
// Base(const Base& base): data(base.data) { } // 如果不手写深拷贝构造函数,这是自动生成的默认的拷贝构造
Base(const Base& base) {data = new int(*base.data);}
~Base() {std::cout << "~Base()" << std::endl;}
private:
int* data;
};
对于一个包含指针成员变量的类,我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。
但是如下列代码:
Base getBase()
{
return Base(); // 无参构造一次,临时变量拷贝构造第一次(本次可能会被编译器优化)
}
Base base = getBase(); // 赋值给base拷贝构造第二次
这样拷贝构造函数一共被调用了两次,申请空间又释放内存,效率比较低。
C++11使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量,将原来的指针成员指向nullptr,这样能够避免多次的申请释放。
class Base
{
public:
Base(): data(new int(0)) { }
// Base(const Base& base): data(base.data) { } // 如果不手写深拷贝构造函数,这是自动生成的默认的拷贝构造
Base(const Base& base) {data = new int(*base.data);}
Base(Base&& base): data(base.data) {base.data = nullptr;}
~Base() {std::cout << "~Base()" << std::endl;}
private:
int* data;
};
Base getBase()
{
return Base(); // 无参构造一次,临时变量拷贝构造(本次可能会被编译器优化)
}
Base base = getBase(); // 赋值给base移动构造
C++11提供了std::move()方法来将左值转换为右值。
class Base
{
public:
Base(): data(new int(0))
{
std::cout << "无参构造" << std::endl;
}
Base(const Base& base)
{
data = new int(*base.data);
std::cout << "拷贝构造" << std::endl;
}
Base(Base&& base): data(base.data)
{
base.data = nullptr;
std::cout << "移动构造" << std::endl;
}
// 当存在移动构造函数、移动赋值函数时,默认的拷贝赋值会被标记成deleted,调用时编译报错,需要手写实现
Base& operator=(const Base& base)
{
data = new int(*base.data);
std::cout << "拷贝赋值" << std::endl;
return *this;
}
Base& operator=(Base&& base)
{
data = base.data;
base.data = nullptr;
std::cout << "移动赋值" << std::endl;
return *this;
}
~Base()
{
std::cout << "~Base()" << std::endl;
}
private:
int* data;
};
Base base1 = Base();
Base base2 = Base();
Base base3(base1); // 调用拷贝构造函数
Base base4(std::move(base1)); // 调用移动构造函数,通过std::move()方法把左值转换成右值,再调用移动构造函数把右值绑定到右值引用上。
// 此时base1的内部指针已经失效了!不要使用
Base base5;
base5 = base2; // 调用拷贝赋值函数
Base base6;
base6 = std::move(base2); // 调用移动赋值函数,通过std::move()方法把左值转换成右值,再调用移动赋值函数把右值绑定到右值引用上。
// 此时base2的内部指针已经失效了!不要使用
如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!(常量左值,使用 const T&,既可以绑定左值又可以绑定右值)
C++11中的所有容器都实现了移动构造函数。move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。
move对于拥有如内存、文件句柄等资源的成员的对象有效,但如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。
完美转发
完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
template<typename T>
void function(T t)
{
otherdef(t);
}
如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:
- 如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;
- 如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。
显然,function() 函数模板并没有实现完美转发,有以下原因:
- 参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;
- 无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。
C++11 标准引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
仍以 function() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:
template <typename T>
void function(T&& t)
{
otherdef(t);
}
此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:
int n = 10;
int & num = n;
function(num); // T 为 int&
int && num2 = 11;
function(num2); // T 为 int &&
其中,由 function(num) 实例化的函数底层就变成了 function(int & & t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。要知道,C++98/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。
在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。
通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。仍以 function 模板函数为例,如下演示了该函数模板的用法:
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t)
{
cout << "lvalue\n";
}
void otherdef(const int & t)
{
cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t)
{
otherdef(forward<T>(t));
}
int main()
{
function(5);
int x = 1;
function(x);
return 0;
}
//程序执行结果为:
//rvalue
//lvalue
此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。