【问题标题】:In what scenarios should I expect to explicitly need to implement a move constructor and move assignment operator?在什么情况下我应该明确需要实现移动构造函数和移动赋值运算符?
【发布时间】:2012-04-15 11:10:59
【问题描述】:

鉴于一个类实际上是可移动的,手动实现类的移动构造函数和移动赋值运算符很快就会变得乏味。

我想知道什么时候这样做实际上是一个沉重、沉重、过早的优化?

例如,如果一个类只有微不足道的 POD 数据或本身定义了移动构造函数和移动赋值运算符的成员,那么我猜编译器要么只是优化大量的垃圾(在POD),否则使用成员的移动构造函数和移动赋值运算符。

但这能保证吗?在什么情况下我应该期望明确需要实现移动构造函数和移动赋值运算符?

编辑: 正如下面 Nicol Bolas 在https://stackoverflow.com/a/9966105/6345 对他的回答的评论中提到的,使用 Visual Studio 11 Beta(及之前)no 移动构造函数或移动赋值运算符永远自动生成。参考:http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx

【问题讨论】:

    标签: c++ optimization c++11 move-semantics


    【解决方案1】:

    如果您发现自己正在实施,任何一个:

    • 析构函数
    • 复制构造函数
    • 复制分配

    那么你应该问问自己是否需要实施移动构造。如果您“= default”上述任何一项,您应该问自己是否还应该“= default”移动成员。

    更重要的是,您应该记录和测试您的假设,例如:

    static_assert(std::is_nothrow_default_constructible<A>::value, "");
    static_assert(std::is_copy_constructible<A>::value, "");
    static_assert(std::is_copy_assignable<A>::value, "");
    static_assert(std::is_nothrow_move_constructible<A>::value, "");
    static_assert(std::is_nothrow_move_assignable<A>::value, "");
    static_assert(std::is_nothrow_destructible<A>::value, "");
    

    【讨论】:

    • 我知道,当复制构造函数是手工制作时(和赋值等效),移动构造函数的生成最近发生了变化。在更改之前有一个关于= default 效果的讨论,如果我没记错的话……= default 实际上最终不会生成移动构造函数/赋值吗?
    • @Matthieu : §12.8/11 描述了将类的默认复制/移动构造函数定义为已删除的条件。
    • @ildjarn:是的,但我认为它最近发生了变化。而且我不确定之前是什么会导致删除,一旦执行 DR 的决议也不会。
    • @Matthieu:啊,我在读 N3337;如果规则发生了变化,我当然对此一无所知。 :-]
    • @MatthieuM.:我相信你指的是open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1402,它目前处于就绪状态。这意味着它还不是官方的,但很可能会在今年秋天被投票通过工作草案。此更改将意味着移动成员不会像现在那样经常被隐式删除。我认为这是一个非常好的改变。但这种变化也是我强调 static_assert 测试的部分动机。每个人都在学习一个新的规范,而规范本身就是一个移动的目标,我们生活在困难时期。测试,测试,测试。
    【解决方案2】:

    首先,移动语义只对拥有任何类型资源的类有帮助。 “平面”类根本没有从中受益。

    接下来,您应该使用“构建块”构建您的类,例如 vectorunique_ptr 等,它们都处理资源的基本低级细节。如果你的类是这样完成的,你根本不需要写任何东西,因为编译器会为你正确地生成成员。

    如果您需要为记录等编写析构函数,则将禁用移动 ctor 的生成,因此您需要 T(T&amp;&amp;) = default; 用于支持它的编译器。否则,这是唯一可以自己编写这样一个特殊成员的地方之一(好吧,除非你编写这样一个“积木”)。

    请注意,析构函数和构造函数中的日志记录可以更简单地完成。只需从记录构造/破坏的特殊类继承即可。或者让它成为一个成员变量。有了这个:

    tl;dr 让编译器为您生成特殊成员。这也适用于复制构造函数和赋值运算符以及析构函数。不要自己写。

    (好吧,也许是赋值运算符,如果您可以将它们识别为瓶颈并想要优化它们,或者如果您想要特殊的异常安全或类似的东西。不过,“构建块”应该已经提供了所有这些。)

    【讨论】:

    • 请注意,如果您想要 基本 异常安全并且有一个可能引发复制分配的成员,那么您很可能 必须 来实现自己复制作业,即使该成员的班级提供强大的例外保证。这是因为默认实现会进行成员分配,如果除最后一个成员之外的任何对象在分配时抛出,这可能会导致部分分配(因此可能无效)对象。
    • 标准容器只能帮助您处理“简单”资源(主要是内存);对于资源,例如在外部设备上分配,您需要编写特殊的分配器。然后,编写自己的“构建块”类通常更实用。
    • @leftaroundabout:是的,但是你要拥有多少个?也许是 4 个?
    • @leftaroundabout:通过指定删除器,你可以让unique_ptrshared_ptr管理你想要的任何东西。
    【解决方案3】:

    每次默认行为不受欢迎或每次默认行为已被删除但您仍然需要它们时执行此操作。

    移动的编译器默认行为是调用成员和基移动。对于平面类/内置类型,这就像复制一样。

    问题通常存在于持有指针或值表示资源(如句柄或特定索引等)的类中,其中移动需要将值复制到新位置,但也需要将旧位置设置为某个“空状态”值”可被析构函数识别。在所有其他情况下,默认行为都可以。

    当您定义一个副本(并且编译器删除默认移动)或移动(并且编译器删除默认副本)并且您需要它们时,也可能会出现问题。在这些情况下,重新启用默认设置可能就足够了。

    【讨论】:

      【解决方案4】:

      在哪些情况下我应该明确需要实现移动构造函数和移动赋值运算符?

      在以下情况下:

      1. 当您使用 Visual Studio 10 或 11 时。它们实现 r 值引用,但编译器生成移动语义。因此,如果您的类型包含需要移动的成员,甚至包含可移动类型(std::unique_ptr 等),您必须自己编写移动。

      2. 当您可能需要复制构造函数/赋值运算符和/或析构函数时。如果您的类包含使您手动编写复制逻辑或需要在析构函数中进行特殊清理的内容,则很有可能您也需要移动逻辑。请注意,这包括删除复制机制 (Type(const Type &amp;t) = delete;)。如果您不想复制对象,那么您可能还需要移动逻辑或删除 move 函数。

      正如其他人所说,您应该尝试将需要显式移动或复制机制的类型数量保持在最低限度。将这些放在实用程序“叶子”类中,并让您的大多数类依赖于编译器生成的复制和移动函数。除非你使用 VS,否则你不会得到这些......

      注意:移动或复制赋值运算符的一个好技巧是按值获取它们:

      Type &operator=(Type t) { std::swap(*this, t); return *this; }
      

      如果您这样做,它将在一个函数中涵盖移动和复制分配。您仍然需要单独移动和复制 构造函数,但它确实将您必须编写的函数数量减少到 4 个。

      缺点是如果Type 仅由基本类型组成,您实际上复制了两次(第一次复制到参数中,第二次复制到swap)。当然,你必须实现/专门化swap,但这并不难。

      【讨论】:

      • 那么,在 VS11 中,如果我有一个包含 3 个 std::string 成员的类,那么编译器将不会自动生成将使用 std::string 自己的移动实现的移动内容?跨度>
      • @JohannGerell:您可以从您的陈述中删除“如果我有课……”部分。正确的表述是,anything 的“编译器不会自动生成移动内容”。它是否包含可移动类型并不重要;如果你想要一个移动构造函数,你必须手动实现它。虽然在 VS10 的情况下,有一个借口:当时的标准也没有隐式生成移动构造函数。
      • 我相信你是对的,但我找不到书面的 MS 参考资料 - 你知道吗?
      • 找到它:Stephan 在以 [...] 开头的段落中雄辩地提到它 Rvalue references v3.0 添加了新规则以在某些条件下自动生成移动构造函数和移动赋值运算符.. .blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx
      【解决方案5】:

      对不起。我可能错过了你问题的重点。我将您的问题理解为复制构造函数。

      早在 1990 年代,当我学习 C++ 时,我总是被教导在设计类时编写一个复制构造函数。否则,这可能随着编译器的较新版本而改变,C++ 将在需要的情况下为您生成一个复制构造函数。

      默认的复制构造函数可能并不总是按照你想要的方式工作。如果您的类包含指针,则尤其如此,或者您不希望默认复制构造函数的默认按字节复制。

      最后,我被告知通过编写复制构造函数,您可以精确控制您希望如何复制您的类。

      我希望这会有所帮助。

      【讨论】:

      • "移动复制构造函数" != "复制构造函数"。
      • @octopusgrabbus:我的问题是关于移动语义,而不是复制语义(我对此毫无疑问)。
      • @fontanini:实际上不存在“移动复制构造函数”,只有复制构造函数和(C++11 起)移动构造函数。请注意,该问题仅包含正确的术语“移动构造函数”。
      • “如果您的类包含指针,则尤其如此” - 如果您的类包含拥有原始指针并且不是我的回答中指出的“构建块”,那么它就坏了。
      • @celtschk:我确实先写了“移动复制构造函数”,但在按下“完成”后 5 秒更改了它——他可能在那段时间读过它……
      猜你喜欢
      • 2012-01-18
      • 2016-05-19
      • 2013-06-11
      • 2017-05-15
      • 2014-07-09
      • 2021-05-28
      • 2017-01-17
      • 2020-12-03
      • 2017-01-16
      相关资源
      最近更新 更多