一个 `=default` 移动构造函数是否等同于成员逐一移动构造函数?

163

这是什么?

struct Example { 
    string a, b; 

    Example(Example&& mE) : a{move(mE.a)}, b{move(mE.b)} { }
    Example& operator=(Example&& mE) { a = move(mE.a); b = move(mE.b); return *this; } 
}

等同于此

struct Example { 
    string a, b;

    Example(Example&& mE)            = default;
    Example& operator=(Example&& mE) = default;
}

?


这可能是 https://dev59.com/22445IYBdhLWcg3wg6pm 的重复。 - user2249683
9
“@DieterLücking: 显然不是,尽管它涉及类似的主题,某些答案可能会涵盖相似的内容。然而,我们不应该将每一个关于移动语义的问题都关闭为重复。” - Lightness Races in Orbit
1
请注意,我回答这个问题是因为当时我在寻找一段标准的引用来证明它们是等效的,而被接受的答案并没有做到这一点。所以我找到了引用并添加了我的答案。 - Shafik Yaghmour
1
我还想提一下,在你的示例中,_默认构造函数_是未声明的,而_析构函数_是默认的 - 请参见Howard Hinnant - 编译器隐式声明 - thomas.st
4个回答

76

是的,两者是相同的。

但是

struct Example { 
    string a, b; 

    Example(Example&& mE)            = default;
    Example& operator=(Example&& mE) = default;
}

这个版本允许您跳过主体定义。

但是,在声明显式默认函数(explicitly-defaulted-functions)时,您需要遵循一些规则:

8.4.2 Explicitly-defaulted functions [dcl.fct.def.default]

A function definition of the form:

  attribute-specifier-seqopt decl-specifier-seqopt declarator virt-specifier-seqopt = default ;

is called an explicitly-defaulted definition. A function that is explicitly defaulted shall

  • be a special member function,

  • have the same declared function type (except for possibly differing ref-qualifiers and except that in the case of a copy constructor or copy assignment operator, the parameter type may be “reference to non-const T”, where T is the name of the member function’s class) as if it had been implicitly declared,

  • not have default arguments.


2
你引用8.4.2节的哪个文档?无论是C++11标准还是N3690都没有在8.4.2/1中包含文本“,并且不具有异常规范”。它们都在8.4.2/2中说:“仅当显式默认函数会被隐式声明为constexpr时,才可以将其声明为constexpr,并且只有在与隐式声明上的异常规范兼容(15.4)时,才可以具有显式异常规范。” - Casey
2
@Casey 很棒的发现!我引用了N3242...我把我的文档搞混了...我更新了我的帖子,引用了N3690!感谢你指出这个问题! - Pierre Fourgeaud
2
如果我将移动构造函数和赋值运算符设置为= default,我能否与对象交换?我不需要将构造函数声明为noexcept吗?我尝试同时为两者都加上noexcept=default,但是这样无法编译通过。 - VF1
声明复制构造函数或赋值运算符或析构函数会阻止默认移动构造函数的生成。如果我们定义了这些函数中的任何一个,我们就必须定义移动构造函数。但是,如果复制构造函数已经被定义,而移动构造函数是由默认关键字创建的呢?它是否按预期工作? - Muhammet Ali Asan
即使 Example 继承了某个类,它们是否仍然相同? - Voyager

41
一个 =default 的移动构造函数是否等同于按成员逐一移动的移动构造函数? 是的更新: 好吧,不总是这样。看看这个例子:
#include <iostream>

struct nonmovable
{
    nonmovable() = default;

    nonmovable(const nonmovable  &) = default;
    nonmovable(      nonmovable &&) = delete;
};

struct movable
{
    movable() = default;

    movable(const movable  &) { std::cerr << "copy" << std::endl; }
    movable(      movable &&) { std::cerr << "move" << std::endl; }
};

struct has_nonmovable
{
    movable    a;
    nonmovable b;

    has_nonmovable() = default;

    has_nonmovable(const has_nonmovable  &) = default;
    has_nonmovable(      has_nonmovable &&) = default;
};

int main()
{
    has_nonmovable c;
    has_nonmovable d(std::move(c)); // prints copy
}

它会打印出:

copy

http://coliru.stacked-crooked.com/a/62c0a0aaec15b0eb

你声明了默认移动构造函数,但是复制操作代替了移动。为什么?因为如果一个类有至少一个不可移动的成员,则显式默认的移动构造函数会被隐式删除(这真是一个双关语)。因此,当你运行has_nonmovable d = std::move(c)时,实际上调用的是复制构造函数,因为has_nonmovable的移动构造函数被删除(隐式),它实际上不存在(即使你通过表达式has_nonmovable(has_nonmovable &&) = default 显式声明了移动构造函数)。
但是,如果non_movable的移动构造函数根本没有声明,那么移动构造函数将用于movable(以及每个具有移动构造函数的成员),而复制构造函数将用于nonmovable(以及每个未定义移动构造函数的成员)。请参见以下示例:
#include <iostream>

struct nonmovable
{
    nonmovable() = default;

    nonmovable(const nonmovable  &) { std::cerr << "nonmovable::copy" << std::endl; }
    //nonmovable(      nonmovable &&) = delete;
};

struct movable
{
    movable() = default;

    movable(const movable  &) { std::cerr << "movable::copy" << std::endl; }
    movable(      movable &&) { std::cerr << "movable::move" << std::endl; }
};

struct has_nonmovable
{
    movable    a;
    nonmovable b;

    has_nonmovable() = default;

    has_nonmovable(const has_nonmovable  &) = default;
    has_nonmovable(      has_nonmovable &&) = default;
};

int main()
{
    has_nonmovable c;
    has_nonmovable d(std::move(c));
}

它会打印出:
movable::move
nonmovable::copy

http://coliru.stacked-crooked.com/a/420cc6c80ddac407

更新:但是,如果你注释掉这行代码:has_nonmovable(has_nonmovable &&) = default;,那么复制将用于两个成员:http://coliru.stacked-crooked.com/a/171fd0ce335327cd - 输出:

movable::copy
nonmovable::copy

所以,在所有地方放置=default可能仍然是有意义的。这并不意味着你的移动表达式总是会移动,但这样做可以增加这种可能性。
另外一个更新:但是如果将has_nonmovable(const has_nonmovable &) = default;这一行注释掉,结果将会是:
movable::move
nonmovable::copy

所以,如果你想知道程序中发生了什么,就只能自己做一切:叹气:

在您的第一个示例中,显式默认的移动构造函数被删除,因为这就是隐式移动构造函数的作用。当使用rvalue时,它是重载决议来消除复制和移动之间的歧义。 - Caleth
2
这就是“隐式声明删除”和“显式声明删除”的区别。当您显式删除某个特殊成员时,它仍然保留在重载集中,但如果选择该成员将导致程序非法,则程序将出现错误。而当它被隐式删除时,它将从重载集中移除。请参见《被删除的隐式声明移动构造函数》(https://en.cppreference.com/w/cpp/language/move_constructor)。 - Caleth
1
只有在成员移动不符合规范时,我才不将其视为不同。 - Caleth
6
@Caleth,但我是这样的。对我来说,如果编译器只给我一个消息:“不能显式声明默认移动构造函数,因为它将是不正确的”,那会更好。如果没有这个消息,当移动构造函数被隐式删除时,我会认为我的移动表达式实际上是移动,而实际上它根本不是移动表达式。显式表达式使隐式的事情变得明确。这非常令人困惑。 - anton_rh
为什么编译器的错误信息不能更有帮助呢?这门语言对于标准委员会之外的人来说变得越来越难以使用。仅仅是初始化规则就太复杂了,难以记住。编译器应该以一种有帮助的方式呈现这些细节。 - undefined
显示剩余5条评论

33

是的,一个默认的移动构造函数会对其基类和成员执行逐个移动,因此:

Example(Example&& mE) : a{move(mE.a)}, b{move(mE.b)} { }

相当于:

Example(Example&& mE)                 = default;

我们可以通过查看C++11标准草案的第12.8“复制和移动类对象”13段来了解这一点(下文将强调重点):

默认值化且未定义为已删除的复制/移动构造函数如果被odruse(3.2)或在其首次声明后显式地缺省,则会隐式定义。[注意:即使实现省略了复制/移动构造函数的odr-use(3.2, 12.2),也会隐式定义它。——注释][...]

和第15段,其中写道:

对于非联合类X,隐式定义的复制/移动构造函数执行成员和基类的逐个复制/移动操作。[注意: 非静态数据成员的花括号或等号初始化程序将被忽略。另请参见12.6.2中的示例。——注释] 初始化的顺序与用户定义的构造函数(见12.6.2)中基类和成员的初始化顺序相同。设x为构造函数的参数或(对于移动构造函数)引用参数的xvalue。每个基类或非静态数据成员都以适合其类型的方式复制/移动:

  • 如果该成员是数组,则直接使用x的相应子对象进行直接初始化;
  • 如果成员m具有rvalue引用类型T&&,则使用static_cast(x.m)进行直接初始化;
  • 否则,将使用x的相应基类或成员对该基类或成员进行直接初始化。

虚基类子对象只能由隐式定义的复制/移动构造函数初始化一次(见12.6.2)。


-3

除非是非常病态的情况...是的。

更准确地说,您还必须考虑示例可能具有的任何基础,其规则完全相同。首先是基础 - 按声明顺序排列,然后是成员,始终按声明顺序排列。


2
但是代码无法改变子对象构造的顺序。语言忽略构造函数成员初始值列表的顺序,并始终以一致的顺序构造(和析构)类子对象。因此,更改该顺序不会导致构造函数不等效。 - aschepler

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