在C++11中, 这个问题的答案就是右值引用. 右值引用是一种新的引用类型, 但它仅能绑定在右值上, 语法是T &&, 我们将原来C++03/98中的引用类型T &称为左值引用. (注意, T &&不是"对引用的引用", 就是右值引用, C++中没有"引用的引用"这种东西).
现在, 我们有两种引用: 左值引用与右值引用, 如果再加上const修饰符, 我们能得到四种引用类型, 下图是一个表格, 展示了何种表达式能绑定到何种引用上:
lvalue const lvalue rvalue const rvalue --------------------------------------------------------- X& yes const X& yes yes yes yes X&& yes const X&& yes yes是不是有点蛋疼呢? 其实实践中, 你完全可以把上表中的最后一行抹掉, const X&&代表了一种对右值的, 不可更改其值的引用, 这种类型你告诉我有什么用?
所以, 右值引用其实就是一种引用类型, 但它仅能绑定在右值上
注意, 此时我们对值类别的讨论依然没有超出C++03的范畴, 我们仅是介绍了一种新的引用类型: 右值引用
隐式转换C++在进行函数调用的时候, 默认会执行一步类型转换, 比如下面就是一个生动的例子:
#include <iostream> class Fuck { friend std::ostream & operator << (std::ostream & out, const Fuck & fuck) { return out << "Fuck[" << fuck._name << "]"; } private: std::string _name; public: Fuck(const std::string & name) { // 该构造函数可以在函数调用时, 将std::string隐式的转换成Fuck对象 _name = name; } }; // 这个函数接受右值引用参数 void Jesus(Fuck && fuck) { std::cout << fuck << std::endl; } int main(void) { // 我们传递给Jesus的参数其实是 std::string 类型 // 在函数调用时会被转换成 Fuck 类型 // 并且由于表达式 std::string("shit") 是一个右值 // 所以转换后的 Fuck 对象也是一个右值 // 故能匹配调用Jesus成功 Jesus(std::string("shit")); // 这里的fuck是一个左值, 所以调用Jesus会失败, 因为Jesus仅接受右值引用参数 // 左值是不能匹配函数参数表的 Fuck fuck("you"); Jesus(fuck); return 0; }上例中的Jesus函数接受右值引用参数, 但实际调用的时候我们传递的是std::string("shit"), 这是一个类型为std::string的右值, 但经过类型转换被转换成Fuck类型, 这个过程中相当创建了两个临时对象:
std::string("shit")创建了一个临时的std::string对象
Jesus函数的调用, 由于参数的自动类型转换, 相当于再创建了一个临时的Fuck对象
最终在函数内部, 右值引用参数绑定的是2中创建的那个临时的Fuck对象.
上例中Jesus(fuck)的调用是失败的, 并且无法成功编译, 原因在于fuck是一个左值, 不匹配函数参数表.
移动构造函数右值引用一个很重要的应用场合就是作为构造函数的参数, 即所谓的移动构造函数. 其目的是从右值中夺取资源初始化当前对象, 以节省拷贝开销.
在C++11中, std::auto_ptr<T>这个模板类被正式盖上了废弃的章, 取而代之的是std::unique_ptr<T>, 上位的手段就是右值引用. 下面我们会写一个简化版的unique_ptr的实现, 首先, 我们需要将指针类型包裹起来, 并且重载->与*操作符以提供更好的使用体验:
template<typename T> class unique_ptr{ private: T * _ptr; public: T* operator->() const { return _ptr; } T& operator*() const { return *_ptr; } };然后给它加上一个构造函数与析构函数, 构造函数的目的是接管对象, 析构函数用以释放对象:
explicit unique_ptr(T * p = nullptr) { _ptr = p; } ~unique_ptr() { delete _ptr; }接下来就是有意思的地方: 我们来写一个移动构造函数:
unique_ptr(unique_ptr && source) { _ptr = source.ptr; source._ptr = nullptr; }这个移动构造函数所做的事情, 其实就是上面我们说的auto_ptr中的拷贝构造函数做的事情, 但是: 这个移动构造函数仅能通过右值去调用. 这样就避免了像auto_ptr那样, 掠夺左值内部资源的危险操作.
unique_ptr<Shape> a(new Triangle); unique_ptr<Shape> b(a); // 这一步调用不能成功, 因为a是一个左值, 并且我们没有定义任何拷贝构造函数 unique_ptr<Shape> c(make_triangle()); // 没有毛病, 因为表达式 `make_triangle()`的值是右值, c其实内部掠夺了`make_triangle()`表达式值的资源b(a)是不能通过编译的, 这是因为:
由于我们已经显式的定义了一个移动构造函数, 所以编译器不再提供默认的拷贝构造函数的实现
a是一个左值, 并不能匹配移动构造函数. 而它想匹配的拷贝构造函数, 没有实现
这种行为就避免了像auto_ptr那样, 对左值资源的错误掠夺
移动赋值操作符我在先前的陈述中一直避免使用移动赋值操作符这个术语, 这是我个人的习惯, 因为我更习惯将其称之为使用右值引用作为参数的=操作符重载.