在从函数返回值时使用std::move(),以避免复制。

62
考虑一个支持默认移动语义的类型T。还考虑下面的函数:
T f() {
   T t;
   return t;
}

T o = f();

在旧的C++03中,一些非最优化的编译器可能会调用两次拷贝构造函数,一次是为了"返回对象",一次是为了o
在C++11中,由于f()内的t是一个左值,那些编译器可能会像以前一样调用一次拷贝构造函数,然后为o调用移动构造函数。
可以说,避免第一次"额外拷贝"的唯一方法是在返回时移动t吗?
T f() {
   T t;
   return std::move(t);
}

这里 有一个类似的问题。 - BЈовић
3个回答

60

不。只要本地变量在return语句中符合复制省略的条件,它就会绑定到一个右值引用上,因此在您的示例中,return t;return std::move(t);相同,关于哪些构造函数是符合条件的。

但请注意,return std::move(t);会防止编译器进行复制省略,而return t;不会,因此后者是首选样式。[感谢@Johannes的更正。]如果发生复制省略,则使用移动构造的问题将成为无意义的问题。

请参见标准中的12.8(31, 32)。

还请注意,如果T具有可访问的复制但已删除的移动构造函数,则return t;将无法编译,因为必须首先考虑移动构造函数;您必须说出类似于return static_cast<T&>(t);才能使其工作:

T f()
{
    T t;
    return t;                 // most likely elided entirely
    return std::move(t);      // uses T::T(T &&) if defined; error if deleted or inaccessible
    return static_cast<T&>(t) // uses T::T(T const &)
}

谢谢。那么移动构造函数的调用次数呢?在一些符合 c++03 标准的编译器中,它是否可能与拷贝构造函数的调用次数相同,都为 2? - Martin
16
它并不相同。仅在于移动构造函数是否会被调用方面是相同的。如果你写return std::move(t);,如果编译器不知道它的作用,那么移动构造函数 必须 被调用。如果你写return t;,即使移动构造函数可能具有副作用,移动构造函数的调用也可以省略。 - Johannes Schaub - litb
1
还有需要注意的是,这仅适用于返回值本身,而不适用于像这样的情况:return std::make_pair(a, b);,在这种情况下,如果需要,应明确移动ab - GManNickG
@GManNickG 谢谢你的留言。请问您(或其他人)能否详细说明一下对于pair或tuple应该如何处理呢? 如果我在上一行创建了pair,然后在下一行返回它,这会有什么不同吗?我哪里做错了吗? - Sold Out

18

不是的,最佳实践是直接使用 return t;

如果类 T 具有未删除的移动构造函数,并注意到 t 是局部变量,则可以对 return t 进行拷贝省略,它会像 return std::move(t); 一样移动构造返回的对象。然而,return t; 仍然有资格进行拷贝/移动省略,因此构造可能会被省略,而 return std::move(t) 则总是使用移动构造函数构造返回值。

如果类 T 的移动构造函数被删除,但复制构造函数可用,则 return std::move(t); 将无法编译通过,而 return t; 仍然可以使用复制构造函数进行编译。与 @Kerrek 所提到的不同,t 并未绑定到右值引用。对于有资格进行拷贝省略的返回值,存在两个阶段的重载决议——先尝试移动构造,然后是复制构造,移动和复制都可能被省略。

class T
{
public:
    T () = default;
    T (T&& t) = delete;
    T (const T& t) = default;
};

T foo()
{
    T t;
    return t;                   // OK: copied, possibly elided
    return std::move(t);        // error: move constructor deleted
    return static_cast<T&>(t);  // OK: copied, never elided
}

如果return表达式是左值且不符合复制省略条件(最可能的情况是返回一个非本地变量或左值表达式),但你仍然想避免复制,std::move会很有用。但请记住,最佳实践是让复制省略尽可能发生。
class T
{
 public:
    T () = default;
    T (T&& t) = default;
    T (const T& t) = default;
};

T bar(bool k)
{
    T a, b;
    return k ? a : b;            // lvalue expression, copied
    return std::move(k ? a : b); // moved
    if (k)
        return a;                // moved, and possibly elided
    else
        return b;                // moved, and possibly elided
}

标准中的12.8(32)描述了该过程。

12.8 [class.copy]

32 当满足复制操作省略的条件或除了源对象是函数参数之外,将满足这些条件,并且要复制的对象由lvalue指定时,首先进行重载分辨率以选择用于复制的构造函数,就好像对象由rvalue指定一样。如果重载分辨率失败,或所选构造函数的第一个参数的类型不是对象类型(可能是cv-qualified)的rvalue引用,则再次执行重载分辨率,将对象视为lvalue。[注意:无论是否发生复制省略,都必须执行这两个阶段的重载分辨率。它确定要调用的构造函数,即使调用被省略,也必须能够访问所选的构造函数。 ——注]


如果类T中的移动构造函数被删除但复制构造函数可用,则return t;仍然可以使用复制构造函数进行编译。但是,这似乎是不正确的(http://cpp.sh/2jxhuy)。 - Sasha

3

好的,我想对此发表评论。这个问题(以及答案)让我相信,在返回语句中不需要指明std::move是没有必要的。然而,当我处理我的代码时,我刚刚学到了一个不同的教训。

因此,我有一个函数(实际上是一种专业化),它接受一个临时对象并将其返回。(通用函数模板做其他事情,但专业化执行身份操作)。

template<>
struct CreateLeaf< A >
{
  typedef A Leaf_t;
  inline static
  Leaf_t make( A &&a) { 
    return a;
  }
};

现在,这个版本在返回时调用了A的复制构造函数。如果我将返回语句更改为
Leaf_t make( A &&a) { 
  return std::move(a);
}

然后调用 A 的移动构造函数,我可以在那里进行一些优化。

这可能并不完全符合您的问题。但是错误地认为永远不需要使用 return std::move(..) 是错误的。 我曾经也这样认为。不再了 ;-)


3
这与原问题不同。原问题是关于返回局部变量x的,当x是局部变量时,return x更好,因为编译器会把x视为rvalue,因为它知道x是局部变量。但当x是引用时,编译器不会给予特殊处理。由于你示例中变量"a"的类型是"A&",因此需要使用move将其改变为"A&&"。 - Doug Cook - MSFT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接