C++11 右值和带有 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();
I'm trying to understand rvalue references and move semantics of C++11.
What is the difference between these examples, and which of them is going to do no vector copy?
First example:
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Second example:
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();
Third example:
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();
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(6)
正如第一个答案的评论中已经提到的,return std::move(...);
构造可以在除返回局部变量之外的情况下产生影响。这是一个可运行的示例,记录了当您返回带有和不带有 std::move()
的成员对象时会发生什么:
#include <iostream>
#include <utility>
struct A {
A() = default;
A(const A&) { std::cout << "A copied\n"; }
A(A&&) { std::cout << "A moved\n"; }
};
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;
}
大概,仅 return std::move(some_member);
如果您确实想要移动特定的类成员,例如在 class C
表示短期适配器对象且唯一目的是创建 struct A
实例的情况下,这是有意义的。
请注意,即使 class B
对象是 R-,struct A
总是如何从 class B
中复制价值。这是因为编译器无法告知 class B
的 struct A
实例将不再被使用。在 class C
中,编译器确实从 std::move()
获取了此信息,这就是 struct A
被移动的原因/em>,除非class C
的实例是常量。
第一个示例
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
定义,您可以像通过值捕获它一样使用它。这与以下内容非常相似:
const std::vector<int>& rval_ref = return_vector();
除了在我的重写中,您显然不能以非常量方式使用 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
在 return 语句中被隐式地视为右值。它将通过返回值优化返回(无复制,无移动),或者如果编译器决定它无法执行 RVO,则它 将使用向量的移动构造函数来执行返回。仅当不执行 RVO,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。
简单的答案是,您应该像编写常规引用代码一样为右值引用编写代码,并且在 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 invoked:
// 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
所做的那样)。您仍然需要 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,新标准也表明在进行回报时,移动构建优先于复制,我相信。
我确实相信您的第二个示例会导致未定义的行为,因为您正在返回对局部变量的引用。
None of those will do any extra copying. Even if RVO isn't used, the new standard says that move construction is preferred to copy when doing returns I believe.
I do believe that your second example causes undefined behavior though because you're returning a reference to a local variable.