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

问题描述 投票:0回答:3

考虑支持默认移动语义的类型 T。还要考虑下面的函数:

T f() {
   T t;
   return t;
}

T o = f();

在旧的 C++03 中,一些非最佳编译器可能会调用复制构造函数两次,一次用于“返回对象”,一次用于

o

在 c++11 中,由于

t
中的
f()
是左值,这些编译器可能会像以前一样调用一次复制构造函数,然后调用 o 的移动构造函数。

避免第一个“额外副本”的唯一方法是返回时移动

t
,这样说是否正确?

T f() {
   T t;
   return std::move(t);
}
c++ move-semantics stdmove
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 &)
}

18
投票

不。最好的做法就是直接

return t;

如果类

T
的移动构造函数未删除,并且注意
t
是一个局部变量,
return t
适合复制省略,则它 move 会像
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 [类.副本]

32 当满足或将满足复制操作的省略条件(源对象是函数参数,并且要复制的对象由左值指定)时,重载决策为该对象选择构造函数首先执行复制,就好像该对象是由右值指定的一样。如果重载决策失败,或者所选构造函数的第一个参数的类型不是对象类型的右值引用(可能是 cv 限定的),则将再次执行重载决策,并将该对象视为左值。 [ 注意:无论是否发生复制省略,都必须执行此两阶段重载决策。如果不执行省略,它确定要调用的构造函数,并且即使省略调用,所选构造函数也必须可访问。 ——尾注]


3
投票

好的,我想对此发表评论。这个问题(和答案)让我相信没有必要在返回语句上指定

std::move
。然而,在处理我的代码时,我只是认为这是一个不同的教训。

所以,我有一个函数(它实际上是一个专业化),它接受一个临时值并返回它。 (通用函数模板执行其他操作,但特化执行恒等操作)。

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

现在,这个版本在返回时调用

A
的复制构造函数。如果我将 return 语句更改为

Leaf_t make( A &&a) { 
  return std::move(a);
}

然后

A
的移动构造函数被调用,我可以在那里做一些优化。

它可能不是 100% 符合您的问题。但认为

return std::move(..)
从来没有必要的想法是错误的。我以前也这么认为。不再是了;-)

© www.soinside.com 2019 - 2024. All rights reserved.