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

我们在上面那个小例子的基础上, 写这样的一个函数, 然后试图去调用它:

Foo && TryToReturnAnRvalueReference() { std::cout << "create auto variable inside func then return std::move(it)" << std::endl; Foo foo; return std::move(foo); } int main(void) { Foo foo5 = TryToReturnAnRvalueReference(); std::cout << "foo5._pod outside function == " << foo5._pod << std::endl; std::cout << "--------------------" << std::endl; Foo foo6(TryToReturnAnRvalueReference()); std::cout << "foo6._pod outside function == " << foo6._pod << std::endl; std::cout << "--------------------" << std::endl; return 0; }

这段代码的输出在不同的编译器下面还有点不一样, 在clang++ 3.4.2编译后, 长下面这样:

create auto variable inside func then return std::move(it) constructor: _pod == 0x1b0a010 destructor: _pod == 0x1b0a010 <- 观察这里, 函数内的自动变量在返回之前就析构了 move constructor <- 完事在这里调用了移动构造函数. 将一个已经不存在(已被析构)的对象中的已被释放(被析构函数释放)的资源进行移动 foo5._pod outside function == 0x7ffd16ec8a60 <- 导致main中的foo5已经放飞自我了 -------------------- create auto variable inside func then return std::move(it) constructor: _pod == 0x1b0a010 destructor: _pod == 0x1b0a010 <- 和foo5的症状基本一样 move constructor foo6._pod outside function == 0x7ffd16ec8a48 -------------------- destructor: _pod == 0x7ffd16ec8a48 *** Error in `./bin/hello': free(): invalid pointer: 0x00007ffd16ec8a48 *** ======= Backtrace: ========= //.... 输出了错误发生时的调用栈 ======= Memory map: ======== //.... 输出了错误发生时的进程内存表

clang++的编译二进制在运行后, 通过echo $?查看二进制的最终返回值, 会发现进程临死前发出的呻吟错误码不为0, 也就是说, clang认为这是一段导致进程crash掉的代码.

而上面这段代码经过g++ 4.8.5编译后, 输出长下面这样:

create auto variable inside func then return std::move(it) constructor: _pod == 0x9c6010 destructor: _pod == 0x9c6010 <- 观察这里, 函数内的自动变量在返回之前就析构了 move constructor foo5._pod outside function == 0 <- main中的foo5没有放飞自我, 而是其资源句柄_pod字段的值, 被移动构造函数置为了0. -------------------- 合理的解释就是, 函数内的自动变量在析构之后, 内存置0了而已 create auto variable inside func then return std::move(it) constructor: _pod == 0x9c6010 destructor: _pod == 0x9c6010 move constructor foo6._pod outside function == 0 -------------------- destructor: _pod == 0 destructor: _pod == 0

g++编译的二进制在运行后, 通过echo $?查看二进制的最终返回值, 是0, 也就是说, g++暂且不认为进程崩掉了. 但这也并不代码你用g++来做开发工作就能写这样的代码!!

上面的编译过程中, 编译参数中的 -O均被设置为-O0

总结一下, 截止目前为止, C++11提供的所谓的右值引用+移动语义, 只能用在两个场合:

函数调用时的参数传递(通过移动构造函数)

对象之间的相互拷贝(通过移动赋值操作符)

而关于函数返回这里, 从C语言一路沿袭下来的内存模型(函数退栈返回的时候, 返回值对象在内存或寄存器中, 是直接改名"移动"的, 而不是进行拷贝, 这是编译器的成果: 汇编层面的行为, 与程序员写的任何构造函数都没关系)决定了, 这和C++11中的右值引用或移动语义没有任何卵关系. 当然, 右值引用在一些很特殊的条件下, 可以作为函数的返回值, 但最佳实践的建议是:

不要这样做

如果你非要这样做, 不要用std::move(函数中的自动变量或临时变量)这种方式去返回

将资源移动给类成员(小坑)

我们来看下面这段代码, 然后你猜一猜它能不能编译通过, 为了你阅读方便, 我把完整的unique_ptr的定义都附带上了:

#include <iostream> #include <utility> 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(const unique_ptr & other) = delete; // 析构函数 ~unique_ptr() { delete _ptr; } // 移动构造函数 unique_ptr(unique_ptr && source) { _ptr = source._ptr; source._ptr = nullptr; } // 移动赋值操作符, 使用了 copy-and-swap idiom unique_ptr & operator = (unique_ptr source) { std::swap(_ptr, source.ptr); return *this; } }; class Foo { private: unique_ptr<int> _member; public: // 构造函数 Foo(unique_ptr<int> && param) : _member(param) // <-- 关键在这里, 编译错误在这里 { } }; int main(void) { return 0; }

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

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