【问题标题】:How to define a move constructor?如何定义移动构造函数?
【发布时间】:2012-03-16 10:44:09
【问题描述】:

我正在 Visual Studio 11 上尝试一些新的 C++11 功能,从移动构造函数开始。我编写了一个名为“MyClass”的简单类,其中包含一个移动构造函数:

class MyClass
{
public:
    explicit MyClass( int aiCount ) 
        : mpiSize( new int( aiCount ) ),
          miSize2( aiCount)
    {
    }

    MyClass( MyClass&& rcOther )
        : mpiSize( rcOther.mpiSize )
        , miSize2( *rcOther.mpiSize )
    {
       rcOther.mpiSize = 0;
       rcOther.miSize2 = 0;
    }

    ~MyClass() 
    {
        delete mpiSize;
    }

private:
    int *mpiSize;
    int miSize2;

};

这里有问题:

  1. 我假设如果我不实现,编译器会为 MyClass 生成一个移动构造函数 - 但似乎不是这样?
  2. 移动构造函数的实现对于 MyClass 是否正确?
  3. 有没有更好的方法来实现 MyClass 的移动构造函数?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:
    1. MSVC++ 在标准的最终版本发布之前实现了移动构造函数。在标准 MSVC++ 的实现所基于的版本中,生成默认移动构造函数的规则比标准的最终版本更严格。请参阅此处:Why is this code trying to call the copy constructor?(特别是 this answer 和上面的 cmets)以获取更多信息。这在 Visual Studio 11 中没有也不会修复,出于某种未知的愚蠢原因,因为它们有其他优先级。

    2. 不,您需要在rcOther 的成员上调用std::move,并使用来自死亡对象的相应成员初始化成员(您错误地命名为miSize):

      MyClass( MyClass&& rcOther )
          : mpiSize( std::move(rcOther.mpiSize) )
          , miSize2( std::move(rcOther.miSize2) )
      {
         rcOther.mpiSize = 0;
      }
      

      对于像intint* 这样的内置类型没有区别,但肯定对于用户定义的类型有区别。

      • 这样做的原因是std::move 只是返回转换为T&& 的参数,一个右值引用,以便为每个调用正确的构造函数(移动构造函数,T(T&&))子对象。如果您不对濒死对象的成员使用std::move,它们将被视为T&,并且您的子对象的复制构造函数(T(T&))将被调用而不是移动构造函数。这非常糟糕,几乎阻碍了您编写移动构造函数的全部目的。


    3. 你在做一些不必要的事情,比如将整数设置为0。你只需要将指针设置为0,这样deleteing就不会删除你创建的新对象的资源。

    另外,如果这不是一个教学练习,您可能需要考虑使用std::unique_ptr 而不是管理您自己的对象的生命周期。这样,您甚至不必为您的课程编写析构函数。请注意,如果您这样做,则必须使用 std::move 从移动构造函数中的垂死成员初始化成员。


    • Konrad Rudolph 在他的回答中发现了这样一个事实,即您的班级管理非自动资源但不遵循 三、四或五的规则。有关更多详细信息,请参阅他的回答。

    【讨论】:

    • 至于“未知的愚蠢原因”,Herb Sutter 在his talk at Going Native 2012 中介绍了微软 C++ 团队的发布时间表。尽管您可能不同意他们的优先事项,但原因似乎既不愚蠢也不未知(尽管在 Microsoft 的网站上可能会更明显)。
    • @AndréCaron 他们没有为 VS11 设置发布日期,因此他们可以轻松地多花一点时间来解决这个问题。
    • @SethCarnegie:他们同样可以使其成为 VS12 并完成整个标准的实施。我的评论是:你可以不同意他们的优先事项(我知道我同意),但原因既不未知也不愚蠢。
    • @AndréCaron 在以前的版本中引入新功能和修复错误是有区别的。但你是对的,我已经编辑了我的答案以反映这一点。我的原始答案主观上受到我对他们决定不修复它的烦恼的影响。
    【解决方案2】:

    为什么编译器不自动生成移动构造函数?

    如果你不这样做,编译器生成一个移动构造函数——在某种程度上。然而,编译器无法猜测你的动机,所以它不知道你的类中的指针做了什么。特别是,它不知道指针授予了内存所有权并且需要被清空。

    move构造函数的实现是否正确?

    移动构造函数是正确的1 但该类的其余部分不正确,您违反了rule of three:您的类需要一个适当的复制构造函数并复制赋值运算符。

    有没有更好的方法来实现移动构造函数?

    编写移动构造函数的更好方法如下:

    MyClass(MyClass&& rcOther)
        : mpiSize(std::move(rcOther.mpiSize))
        , miSize2(std::move(rcOther.miSize2))
    {
        rcOther.mpiSize = 0;
    }
    

    两个cmets:

    • 为什么不直接复制成员,而是取消引用rcOther.mpiSize?虽然这没有错,但它也没有任何意义并且具有误导性。
    • 您不需要将整数归零,因为它没有必要,所以不应该这样做:移动构造函数应该对被移动对象执行的唯一修改是放弃其资源的所有权,以便它可以销毁而不会导致资源被双重删除。

    但更好的方法是依靠已有的设施。在这种情况下,您希望对内存所有权进行建模。裸指针做得很差,您应该改用std::unique_ptr。这样,您不需要实现析构函数或移动构造函数,因为自动生成的方法会做正确的事情。


    1警告:请参阅 Seth 的答案以获得更好的解释,其中提到了 std::move(然而,在这种特殊情况下,这是一个无操作)。

    【讨论】:

    • 我将 mpiSize 声明为用于测试目的的原始 int 指针。你是对的,rcOther.miSize = 0;是不必要的,可以省略。三分法则是什么意思?我只知道两个规则:)(如果定义了复制构造函数,则实现赋值运算符,反之亦然)。
    • @jisaak “两人的规则”不存在。我添加了一个解释三规则的链接。
    • @Seth That’s a matter of debate就正确性而言,三规则仍然是三规则。我同意作为设计指南,我们应该始终使用五法则。
    • @jisaak 不,这很好:重要的是您阻止编译器为您自动生成这些方法,因为这些方法会被破坏。声明它们而不定义它们是可以的,尽管 C++11 提供了更好的机制,通过显式删除它们,例如MyClass(MyClass const&) = delete;
    • 啊,我误解了之前的评论中有一个错字。 " rcOther.miSize = 0; 是不必要的" 应该是 " rcOther.miSize2 = 0; 是不必要的"
    【解决方案3】:

    从 C++14 开始,您可以利用 std::exchange() 便利函数模板来定义移动构造函数。这可能会导致更简洁的移动构造函数定义:

    MyClass(MyClass&& other) noexcept:
       mpiSize(std::exchange(other.mpiSize, nullptr)),
       miSize2(std::exchange(other.miSize2, 0))
    {}
    

    std::exchange(other.mpiSize, nullptr)other.mpiSize 的值替换为nullptr,但返回other.mpiSize 在替换之前的值。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-23
      • 1970-01-01
      相关资源
      最近更新 更多