C ++ 11 rvalues和移动语义混淆(return语句)
我想了解右值引用并移动C ++ 11的语义。
这些例子之间有什么区别,哪些不做矢量拷贝?
第一个例子
std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> &&rval_ref = return_vector();
第二个例子
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();
第三个例子
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();
第一个例子
std::vector<int> return_vector(void) { std::vector<int> tmp {1,2,3,4,5}; return tmp; } std::vector<int> &&rval_ref = return_vector();
第一个例子返回一个由rval_ref
捕获的临时rval_ref
。 这个临时的生命将会超越rval_ref
定义,你可以像使用它的价值一样使用它。 这与以下内容非常相似:
const std::vector<int>& rval_ref = return_vector();
除了在我的重写中,显然不能以非const方式使用rval_ref。
第二个例子
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();
在第二个例子中,你创建了一个运行时错误。 rval_ref现在持有对函数内部被破坏的tmp的引用。 幸运的是,这段代码会立即崩溃。
第三个例子
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();
你的第三个例子大致相当于你的第一个例子。 tmp上的std :: move是不必要的,实际上可能是性能悲观,因为它会阻止返回值优化。
编码你正在做的最好的方法是:
最佳实践
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中一样。 在返回语句中,tmp隐含地被视为一个右值。 它会通过返回值优化(不复制,不移动)返回,或者如果编译器决定不能执行RVO,那么它将使用向量的移动构造函数做返回。 只有当RVO没有执行,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。
他们中的任何一个都不会复制,但是第二个将会引用一个被毁坏的向量。 命名的右值引用在常规代码中几乎不存在。 你把它写成你将如何用C ++ 03写一个副本。
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,并且能够编写一个通用的函数来取得左值或右值引用,这或多或少是正确的。
移动构造函数和移动赋值的一个重要优点是,如果定义它们,编译器可以在RVO(返回值优化)和NRVO(命名的返回值优化)调用失败的情况下使用它们。 这对于从方法中有效地返回像容器和字符串这样的昂贵对象来说是相当大的。
现在,右值引用引起了一些有趣的事情,你也可以将它们用作正常函数的参数。 这允许你编写具有const引用(const foo&other)和右值引用(foo && other)的重载的容器。 即使这个论点过于笨拙,仅仅通过一个构造函数就可以完成:
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容器已经被更新,几乎所有的东西都会移动重载(散列键和值,向量插入等),并且是最能看到它们的地方。
你也可以将它们用于正常的函数,如果你只提供一个右值引用参数,你可以强制调用者创建该对象并让该函数完成移动。 这不仅仅是一个真正的好用的例子,但是在我的渲染库中,我已经为所有加载的资源分配了一个字符串,以便更容易地看到每个对象在调试器中代表什么。 界面是这样的:
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:
// move from temporary TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
要么
// 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));
要么
// 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));
但是这不会编译!
string str("Checkerboard"); TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
本身不是答案,而是指导方针。 大多数情况下,声明本地T&&
变量没有多少意义(就像你用std::vector<int>&& rval_ref
)。 您仍然必须将std::move()
在foo(T&&)
类型的方法中使用。 还有一个问题已经提到,当你试图从函数返回这样的rval_ref
,你会得到标准的引用到被破坏的临时失败。
大多数时候我会采取以下模式:
// Declarations A a(B&&, C&&); B b(); C c(); auto ret = a(b(), c());
您不会持有任何返回的临时对象的引用,因此您可以避免(不经验的)程序员的错误谁想要使用移动的对象。
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();
很明显,有一些情况(尽管很少见),其中一个函数真正返回一个T&&
,它是一个非临时对象的引用,可以移动到对象中。
关于RVO:这些机制通常是有效的,编译器可以很好地避免复制,但是在返回路径不明显的情况下(例外, if
条件决定了你将返回的命名对象,更贵)。
这些都不会做任何额外的复制。 即使RVO不被使用,新的标准也表示,在做回报的时候,我认为移动建设是优选的。
我相信你的第二个例子会导致未定义的行为,因为你正在返回一个局部变量的引用。