考虑支持默认移动语义的类型 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);
}
不。每当
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 &)
}
不。最好的做法就是直接
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 限定的),则将再次执行重载决策,并将该对象视为左值。 [ 注意:无论是否发生复制省略,都必须执行此两阶段重载决策。如果不执行省略,它确定要调用的构造函数,并且即使省略调用,所选构造函数也必须可访问。 ——尾注]
好的,我想对此发表评论。这个问题(和答案)让我相信没有必要在返回语句上指定
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(..)
从来没有必要的想法是错误的。我以前也这么认为。不再是了;-)