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

移动赋值操作符的目的是释放旧资源, 并从=右边获取(夺取)新的资源. 下面我们给unique_ptr实现一个移动赋值操作符

unique_ptr & operator = (unique_ptr && source) { if (this != &source) { delete ptr; ptr = source.ptr; source.ptr = nullptr; } return *this; } };

我们在copy-and-swap idiom中已经讲过了这种写法的缺陷, 上面的写法有两个特点:

它仅能实现资源的移动, 真是移动赋值操作符, 但并不能实现拷贝赋值的语义. 即如果=右边是一个左值, 会编译失败

这个实现只是很直观的在向你展示移动赋值操作符的语义

真正的良好实践, 如我们在copy-and-swap idiom中讲的那样, 应当如下写:

unique_ptr & operator = (unique_ptr source) { std::swap(_ptr, source.ptr); return *this; } };

这个写法有两个特点:

是否需要实现拷贝语义, 要看拷贝构造函数是否存在

避免了代码重复, 异常不安全等缺陷.

这些内容在copy-and-swap我们已经讲过了, 这里就不再重复陈述了.

在左值上实施移动: 掠夺左值的内部资源

有时候我们确实想掠夺一个左值的资源, 并且我们确实明白这样做风险的话, C++11也为我们提供了一个途径: std::move.

在继续讲之前我实再是忍不住要吐槽一下, 这就是C++吸引人的地方, 也是C++ Fucked up的地方: 真他妈是给你自由过了火, 你想怎么整都行, C++恨不得把挖祖坟的能力给你.

在头文件<utility>中, C++11新提供了一个标准库设计:模板函数std::move. 坦白讲这个模板函数的名字取的非常有误导性, 其实std::move并不实施任何与移动有关的操作, 它的功能仅是把一个左值, 转换成一个右值, 从而使得可以调用仅接受右值引用的函数. 其实讲道理这个模板函数应该把名字取成std::cast_to_rvalue或std::enable_move, 但标准已经是这样了, 咱就少吐槽乖乖接受算了.

下面是如何使用它的示例:

unique_ptr<std::string> a(new string("fuck")); unique_ptr<std::string> b(a); // 按先前的讲解, 我们知道这一句会编译错误, 因为unique_ptr并没有实现拷贝构造函数, 而a是一个左传 unique_ptr<std::string> c(std::move(a)); // 这样就可以通过编译了, 但这是一个危险操作: a中的资源被掠夺了, 像我们在讨论auto_ptr的实现时那样 将亡值(xvalue)

std::move最神奇的一点就是, 虽然std::move从表面上把一个左值改成了一个右值, 但std::move本身并没有创建一个新的临时对象. 是的, std::move的求值结果, 本质上还是指向之前的那个对象, 那么, std::move的运算结果, 到底算是左值, 还是右值呢?

在C++11中, std::move的运算结果, 是一种全新的值类别, 叫将亡值(xvalue,(eXpiring value)). , 所以让人头大的事情就来了: 先来看下面这张图:

expressions / \ / \ / \ glvalues rvalues / \ / \ / \ / \ / \ / \ lvalues xvalues prvalues

最底层的三种值类型, 是C++11中每个表达式的值类别: 你要么是一个lvalue(左值), 要么是一个xvalue(将亡值), 要么是一个prvalue(纯右值). 其中lvalue就是C++03中的左值, 而prvalue就是C++03中的右值. 而xvalue, 则是一个全新的概念: 你可以暂时将它理解为, std::move的值类别.

函数返回值的移动(大坑)

截止目前, 我们对移动语义的讨论还未涉及函数返回, 下面是一个函数返回时移动资源的例子:

unique_ptr<Shape> make_triangle() { return unique_ptr<Shape>(new Triangle); } \-----------------------------/ | | return 语句中创建的临时对象中的资源, 被移动到了c中 | 这个过程和移动构造函数无关, 是编译器的优化行为 v unique_ptr<Shape> c(make_triangle());

函数返回的过程中, make_triangle返回的是一个临时量, 用这个临时量去初始化c时, 编译器会自动将临时量的资源移动给c, 特别吊诡的事情是: 这个移动操作的过程, 移动构造函数并没有参与, 拷贝构造函数也没有参与!

函数按值返回时, 发生的诡异的移动行为, 与右值引用无关, 和C++11甚至都没有关系, 这就是一个编译器的优化行为, 这个优化行为诡异的点在于:

按C++98或03标准的眼光去看, c的初始化应当调用拷贝构造函数. 但实际上, 并没有

按C++11标准的眼光去看, c的初始化应当调用移动构造函数. 但实际上, 也没有

既然既没调用拷贝构造函数, 也没调用移动构造函数, 好吧你编译器要搞黑魔法你去搞, 那我把拷贝构造函数和移动构造函数都声明为delete行不行呢? 不行, 编译器(至少是gcc)会提示你: 类缺乏拷贝构造函数, 故函数无法返回.

编译器看起来让你以为, 它在调用拷贝构造函数或者移动构造函数, 但实际上并没有. 它内部实现了一个很诡异的移动操作: 是的, 这个临时量持有的资源, 被转交给了c. 而不是拷贝.

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

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