C++11 rvalues and move semantics confusion (return statement)
我试图理解R值引用并移动C++ 11的语义。
这些例子之间有什么区别,哪一个不做向量复制?
第一实例1 2 3 4 5 6 7 | std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> &&rval_ref = return_vector(); |
第二实例
1 2 3 4 5 6 7 | std::vector<int>&& return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return std::move(tmp); } std::vector<int> &&rval_ref = return_vector(); |
第三例
1 2 3 4 5 6 7 | std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return std::move(tmp); } std::vector<int> &&rval_ref = return_vector(); |
型第一个例子
1 2 3 4 5 6 7 | std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> &&rval_ref = return_vector(); |
号
第一个示例返回一个被
1 | const std::vector<int>& rval_ref = return_vector(); |
除了在我的重写中,你显然不能以非常量的方式使用
第二个例子
1 2 3 4 5 6 7 | std::vector<int>&& return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return std::move(tmp); } std::vector<int> &&rval_ref = return_vector(); |
。
在第二个示例中,您创建了一个运行时错误。
第三个例子
1 2 3 4 5 6 7 | std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return std::move(tmp); } std::vector<int> &&rval_ref = return_vector(); |
第三个例子大致相当于第一个例子。
编写代码的最佳方法是:
最佳实践
1 2 3 4 5 6 7 | std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> rval_ref = return_vector(); |
。
就像你在C++ 03中所做的一样。
型
它们都不会复制,但第二个是指被破坏的向量。命名的右值引用在常规代码中几乎不存在。你写的只是你如何在C++ 03中写一个拷贝。
1 2 3 4 5 6 7 | std::vector<int> return_vector() { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> rval_ref = return_vector(); |
除了现在,矢量是移动的。在大多数情况下,类的用户不会处理它的右值引用。
简单的答案是,您应该为右值引用编写代码,就像常规引用代码一样,并且在99%的情况下,您应该在精神上对待它们。这包括有关返回引用的所有旧规则(即从不返回对局部变量的引用)。
除非您正在编写一个模板容器类,该类需要利用std::forward并能够编写一个采用lvalue或rvalue引用的通用函数,否则这或多或少是正确的。
move构造函数和move赋值的一大优点是,如果定义了它们,编译器可以在调用rvo(返回值优化)和nrvo(命名为返回值优化)失败的情况下使用它们。这对于高效地从方法中按值返回容器和字符串等昂贵的对象来说是非常巨大的。
现在,右值引用的有趣之处在于,您还可以将它们用作普通函数的参数。这允许您为const-reference(const-foo&other)和rvalue-reference(foo&other)编写重载的容器。即使参数太难用一个简单的构造函数调用传递,仍然可以做到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | std::vector vec; for(int x=0; x<10; ++x) { // automatically uses rvalue reference constructor if available // because MyCheapType is an unamed temporary variable vec.push_back(MyCheapType(0.f)); } std::vector vec; for(int x=0; x<10; ++x) { MyExpensiveType temp(1.0, 3.0); temp.initSomeOtherFields(malloc(5000)); // old way, passed via const reference, expensive copy vec.push_back(temp); // new way, passed via rvalue reference, cheap move // just don't use temp again, not difficult in a loop like this though . . . vec.push_back(std::move(temp)); } |
STL容器已经更新为几乎所有内容(散列键和值、向量插入等)都有移动重载,您将在其中看到最多的内容。
也可以将它们用于普通函数,如果只提供右值引用参数,则可以强制调用方创建对象并让函数进行移动。这不仅仅是一个很好的例子,但在我的渲染库中,我已经为所有加载的资源分配了一个字符串,这样就更容易看到调试器中每个对象所代表的内容。接口是这样的:
1 2 3 4 5 6 | TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName) { std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt); tex->friendlyName = std::move(friendlyName); return tex; } |
它是一种"泄漏抽象"的形式,但允许我利用这个事实,我必须在大多数时候创建字符串,并避免对它进行另一次复制。这并不完全是高性能的代码,但它是一个很好的例子,说明了随着人们掌握了这一特性。此代码实际上要求变量要么是调用的临时变量,要么是调用的std::move:
1 2 | // move from temporary TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard")); |
或
1 2 3 | // explicit move (not going to use the variable 'str' after the create call) string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str)); |
或
1 2 3 4 | // explicitly make a copy and pass the temporary of the copy down // since we need to use str again for some reason string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str)); |
但这不会编译!
1 2 | string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str); |
不是答案本身,而是指导方针。在大多数情况下,声明本地
大多数情况下,我会采用以下模式:
1 2 3 4 5 6 | // Declarations A a(B&&, C&&); B b(); C c(); auto ret = a(b(), c()); |
对于返回的临时对象,您不持有任何引用,因此可以避免(没有经验的)希望使用移动对象的程序员错误。
1 2 3 4 5 6 7 8 | auto bRet = b(); auto cRet = c(); auto aRet = a(std::move(b), std::move(c)); // Either these just fail (assert/exception), or you won't get // your expected results due to their clean state. bRet.foo(); cRet.bar(); |
显然,在某些情况下(尽管非常罕见),函数确实返回一个
关于rvo:这些机制通常都可以工作,编译器可以很好地避免复制,但是在返回路径不明显的情况下(例外情况,
型
这些都不会做任何额外的复制。即使没有使用RVO,新的标准说,我相信在做退货的时候,移动结构最好是复制的。
我相信您的第二个示例会导致未定义的行为,因为您将返回对局部变量的引用。
正如第一个答案的注释中所提到的,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #include <iostream> #include <utility> struct A { A() = default; A(const A&) { std::cout <<"A copied "; } A(A&&) { std::cout <<"A moved "; } }; class B { A a; public: operator A() const & { std::cout <<"B C-value:"; return a; } operator A() & { std::cout <<"B L-value:"; return a; } operator A() && { std::cout <<"B R-value:"; return a; } }; class C { A a; public: operator A() const & { std::cout <<"C C-value:"; return std::move(a); } operator A() & { std::cout <<"C L-value:"; return std::move(a); } operator A() && { std::cout <<"C R-value:"; return std::move(a); } }; int main() { // Non-constant L-values B b; C c; A{b}; // B L-value: A copied A{c}; // C L-value: A moved // R-values A{B{}}; // B R-value: A copied A{C{}}; // C R-value: A moved // Constant L-values const B bc; const C cc; A{bc}; // B C-value: A copied A{cc}; // C C-value: A copied return 0; } |
大概,
注意,即使