移动构造函数应该使用const还是非const右值引用?

49

我曾在几个地方看到过有关复制和移动构造函数的推荐签名,如下所示:

struct T
{
    T();
    T(const T& other);
    T(T&& other);
};

复制构造函数需要一个const引用,而移动构造函数需要一个非const右值引用。

但就我所见,这会阻止我在从函数返回const对象时利用移动语义,例如下面的情况:

T generate_t()
{
    const T t;
    return t;
}

测试使用 VC11 Beta,调用的是 T 的拷贝构造函数而不是移动构造函数。即使使用 return std::move(t);,仍然会调用拷贝构造函数。

我可以理解这种行为是有道理的,因为 t 是 const,所以不能绑定到 T&& 上。在移动构造函数签名中使用const T&& 是可行的,也是合理的,但问题在于,由于 other 是 const,如果需要将其成员设置为空,就无法实现 - 只有当所有成员都是标量或具有正确签名的移动构造函数时才能工作。

看起来,在一般情况下确保调用移动构造函数的唯一方法是首先使 t 不是 const,但我不喜欢这样做 - 将事物置为 const 是一个好习惯,我不希望 T 的客户端知道他们必须违反这个规范以提高性能。

因此,我的问题有两个方面;首先,移动构造函数应该使用 const 还是非 const 的右值引用?其次:我的推理是否正确? 我应该停止返回常量对象吗?


2
我认为返回const临时变量的用例有点少。如果您事先声明它,那是因为您想在返回之前对其进行一些操作。如果没有,则简单的return T()就可以了。虽然我仍然可以看到某些用例,但我认为它们很少见。当然,与左值引用相比,无法窃取资源的右值引用实际上没有任何价值。不过这是一个有趣的问题。 - Christian Rau
1
顺便问一下,这段代码不符合 NRVO 吗?所以“应该”发生的是,const 对象 t 被构造到与非 const 返回值相同的位置。然后如果必要,非 const 返回值 可以 通过非 const 移动构造函数(或移动赋值运算符)由调用者移动。在构造的情况下,它再次符合复制省略,并且 t 可以直接构造到使用 generate_t() 调用进行初始化的任何内容中。是什么阻碍了 VC11 进行此优化? - Steve Jessop
3
“保持事物原貌是一种良好的做法”,但如果你想对它们进行修改,比如移动它们,这个做法就不适用了。 - Jonathan Wakely
...其结果是,在有人解释我未能理解移动与复制省略交互的方式之前,我认为你是正确的,即T的客户端不需要违反该形式以提高性能。然而,作为QoI问题,VC11(使用您提供的选项)已经失误了。总的来说,我认为说一个类的客户端不应该考虑哪些对象可以从中移动以提高性能并不完全正确。只是在这种情况下,我认为他们不需要这样做。 - Steve Jessop
@SteveJessop,它是符合复制省略的条件的。它不能被移动,因为它是const,所以移动构造函数不可行,因此选择了复制构造函数,但是复制被省略了。如果该变量是non-const(或移动构造函数采用const T&&这将是不好的),那么移动构造函数将是可行的,但会被省略。因此,唯一的区别应该是移动还是复制是否被省略,但无论如何都应该被省略。 - Jonathan Wakely
显示剩余4条评论
4个回答

40

应该使用非const右值引用。

如果一个对象被放置在只读内存中,即使它的正式生命周期很快就要结束,也不能从它那里窃取资源。在C++中以const创建的对象可以存在于只读内存中(尝试使用const_cast来更改它们会导致未定义行为)。


4
很可能情况并非如此。一个可以移动的对象似乎表明它管理资源,所以在对象构造时必须是可写的,而且它可能有一个析构函数来释放这些资源,因此在销毁过程中也必须是可写的。我怀疑编译器会生成代码来管理将内存页标记为只读状态,并在销毁之前将其标记为可写状态(销毁完成后再次标记为只读状态,因为大多数情况下对象不是单独存在于该内存页中...)。话虽如此,点赞。 - David Rodríguez - dribeas
@David:“我怀疑……”-没错,但在调试模式下可能是有用的功能,特别是在修复遗留的const不正确的代码时。我认为重要的一点是,尽管这件事实际上不会发生,但它是合法的,这就解释了为什么你不能在常量对象的析构函数之前修改它们(特别是当它们持有资源时不能有效地从中移动)。 - Steve Jessop
@SteveJessop:这就是+1的原因:无论对象是否在只读内存中,移出常量对象都会打破合同:您正在修改承诺不触摸的内容。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas:我不明白为什么在销毁期间释放资源需要写入持有这些资源的对象。尽管如此,析构函数会被调用const对象,并且在其析构函数中修改对象是合法的,因此可能需要取消保护内存。我仍然怀疑编译器通常可以省略取消保护,因为在成员变量变得不可访问之前就写入它们似乎是不寻常的。 - Ben Voigt

10

一个移动构造函数通常应该采用一个非const引用。

如果从一个const对象进行移动,通常意味着复制一个对象与从它进行"移动"一样高效。此时通常没有获得移动构造函数的好处。

您的观点也是正确的,如果您有一个可能需要移动的变量,则需要是非const的。

据我所知,这就是为什么Scott Meyers已经在C++11中更改了他关于通过值从函数返回类类型对象的建议。通过const限定的值返回对象确实可以防止无意中修改临时对象,但也会阻碍从返回值进行移动。


3
如果可以从一个常量对象中移动,通常意味着复制对象和从对象“移动”具有同样的效率。 - Patrick Fromberg

9
移动构造函数应该采用非const右值引用。首先,右值引用的const形式没有意义,因为您想要修改它们(以某种方式,您想要将它们“移动”,您希望将它们的内部内容作为自己的)。此外,它们被设计为在不使用const的情况下使用。我认为仅使用const右值引用的唯一用途是Scott Meyers在这个谈话中提到的非常神秘的事情(从时间42:20到44:47)。
在这个问题上,我的推理是否正确?我应该停止返回const对象吗?
我认为这是一个过于笼统的问题,难以回答。在这种情况下,值得一提的是,有一个std :: forward功能可以保留rvalue-ness和lvalue-ness以及const-ness,并且它还可以避免创建临时对象,而正常函数会将传递给它的任何内容都返回。这种返回也会导致右值引用变成左值引用,通常您不希望这样,因此,使用前面提到的功能进行完美转发可以解决问题。
话虽如此,我建议您仅查看我发布链接的谈话。

5
除了其他答案中提到的内容,有时候移动构造函数或函数接受 const T&& 的原因是存在的。例如,如果您将返回 const 对象的函数的结果按值传递给构造函数,则会调用 T(const T&) 而不是预期的 T(T&&)(请参见下面的函数 g)。
这就是删除接受 constT&& 的重载版本以供 std::refstd::cref 使用而不是接受 T&& 的原因。
具体来说,在重载决议期间的优先顺序如下:
struct s {};

void f (      s&);  // #1
void f (const s&);  // #2
void f (      s&&); // #3
void f (const s&&); // #4

const s g ();
s x;
const s cx;

f (s ()); // rvalue        #3, #4, #2
f (g ()); // const rvalue  #4, #2
f (x);    // lvalue        #1, #2
f (cx);   // const lvalue  #2

请查看这篇文章获取更多细节信息。

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