对C++11中的`移动语义`与`右值引用`的介绍与讨论 (8)

在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那样, 对左值资源的错误掠夺

移动赋值操作符

我在先前的陈述中一直避免使用移动赋值操作符这个术语, 这是我个人的习惯, 因为我更习惯将其称之为使用右值引用作为参数的=操作符重载.

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzspdp.html