提问人:Tarantula 提问时间:2/14/2011 最后编辑:Jan SchultkeTarantula 更新时间:9/28/2023 访问量:139120
是否有必要在 return 语句中使用 std::move,是否应该返回右值引用?
Is it necessary to std::move in a return statement, and should you return rvalue references?
问:
我正在尝试理解右值引用并移动 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();
答:
它们都不会复制,但第二个将引用被破坏的向量。命名右值引用几乎从来不存在于常规代码中。你写它就像你在 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();
除了现在,向量被移动了。在绝大多数情况下,类的用户不会处理它的右值引用。
评论
tmp
rval_ref
rval_ref
std::move
std::move
rval_ref
std::vector
std::move
tmp
return
这些都不会进行任何额外的复制。即使不使用 RVO,我相信新标准也表明,在进行退货时,移动结构比复制更可取。
我确实相信您的第二个示例会导致未定义的行为,因为您返回了对局部变量的引用。
第一个例子
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();
在第二个示例中,您创建了一个运行时错误。 现在在函数中保存对 destructed 的引用。如果运气好的话,这段代码会立即崩溃。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();
第三个示例大致等同于第一个示例。on 是不必要的,实际上可能是一种性能悲观,因为它会抑制返回值优化。std::move
tmp
对你正在做的事情进行编码的最好方法是:
最佳实践
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 中一样。 在 return 语句中隐式视为右值。它要么通过返回值优化(不复制,不移动)返回,要么如果编译器决定它不能执行 RVO,那么它将使用 vector 的移动构造函数来执行返回。仅当未执行 RVO 并且返回的类型没有移动构造函数时,复制构造函数才会用于返回。tmp
评论
return my_local;
move
move
简单的答案是,你应该像编写常规引用代码一样为右值引用编写代码,并且你应该在 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();
显然,在有些情况下(尽管相当罕见),函数真正返回 a,这是对非临时对象的引用,您可以将其移动到对象中。T&&
关于 RVO:这些机制通常有效,编译器可以很好地避免复制,但在返回路径不明显的情况下(异常、确定您将返回的命名对象的条件,可能还有其他几个),rrefs 是您的救星(即使可能更昂贵)。if
正如在对第一个答案的评论中已经提到的,除了返回局部变量之外,该构造还可以在其他情况下发挥作用。下面是一个可运行的示例,它记录了在返回有和没有成员对象时发生的情况: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
请注意,即使对象是 R 值,也总是会被复制出。这是因为编译器无法判断 的实例不会再被使用。在 中,编译器确实有来自 的信息,这就是被移动的原因,除非 的实例是常量。struct A
class B
class B
class B
struct A
class C
std::move()
struct A
class C
评论
std::move()
std::move(expression)
std::move(expression)