【问题标题】:(Why) should a move constructor or move assignment operator clear its argument?(为什么)移动构造函数或移动赋值运算符应该清除它的参数吗?
【发布时间】:2017-01-17 17:14:28
【问题描述】:

我正在学习的 C++ 课程中的移动构造函数实现示例如下所示:

/// Move constructor
Motorcycle::Motorcycle(Motorcycle&& ori)
    : m_wheels(std::move(ori.m_wheels)),
      m_speed(ori.m_speed),
      m_direction(ori.m_direction)
{
    ori.m_wheels = array<Wheel, 2>();
    ori.m_speed      = 0.0;
    ori.m_direction  = 0.0;
}

(m_wheelsstd::array&lt;Wheel, 2&gt; 类型的成员,Wheel 只包含 double m_speedbool m_rotating。在 Motorcycle 类中,@ 987654327@ 和m_direction 也是doubles。)

我不太明白为什么需要清除ori 的值。

如果Motorcycle 有任何我们想要“窃取”的指针成员,那么当然,我们必须设置ori.m_thingy = nullptr 以免,例如delete m_thingy 两次。但是,当字段包含对象本身时,这有关系吗?

我问了一位朋友,他们将我指向this page,上面说:

Move 构造函数通常“窃取”参数持有的资源(例如,指向动态分配对象的指针、文件描述符、TCP 套接字、I/O 流、正在运行的线程等),而不是复制它们并离开某些有效但不确定的状态的参数。例如,从std::stringstd::vector 移动可能会导致参数为空。但是,不应依赖此行为。

谁定义了不确定状态的含义?我看不出将速度设置为 0.0 比保持不变更不确定。就像引用中的最后一句话所说的那样——代码不应该依赖于从 Motorcycle 移出的状态,所以为什么还要清理它呢?

【问题讨论】:

  • 我看不出构造函数主体中的三行有什么特别的原因。我也看不出有什么特别的理由为这个类设置一个移动构造函数——它看起来和复制构造函数做的事情完全一样。
  • @Lynn:无论如何,在构造函数中执行 IO 是不好的巫术,恕我直言
  • @PatrickCollins 我同意。我不太喜欢这门课。 :(
  • @IgorTandetnik 它做的事情并不完全相同;它移动了wheels 数组。
  • @immibis 确实如此。然而,Wheel 类的口头描述表明移动这个数组与复制它没有区别。

标签: c++ c++11 move-constructor


【解决方案1】:

它们需要清洁。类的设计者认为将移动的对象零初始化是一个好主意。

请注意,确实有意义的情况是对象管理在析构函数中释放的资源。例如,指向动态分配内存的指针。不修改移出对象中的指针意味着两个对象管理相同的资源。他们的析构函数都会尝试释放。

【讨论】:

  • 您的整个第二段与问题中已经存在的内容是多余的:“如果摩托车有任何指针成员......”等等。
  • @KyleStrand 谢谢。我以指针为例,但我的陈述旨在更笼统。并非所有资源都由指针处理。
  • ....我有种感觉,对于 OP 在他们的问题中的陈述,也可以这样说,但要点是。你的措辞确实更好更准确。
  • @Joshua 不,移出对象的析构函数仍会被调用。
  • @Joshua 这完全公平!不过,我认为移动语义对于低级语言来说是一个非常有趣的概念,所以如果有机会,你应该了解更多。如果你更喜欢非 C++ 上下文,移动语义是 Rust 的核心部分,我敢肯定它是我不知道的其他语言的一个特性。
【解决方案2】:

谁定义了不确定状态的含义?

课程的作者。

如果您查看std::unique_ptr 的文档,您会在以下几行之后看到:

std::unique_ptr<int> pi = std::make_unique<int>(5);
auto pi2 = std::move(pi);

pi 实际上处于非常明确的状态。它reset()pi.get()等于nullptr

【讨论】:

    【解决方案3】:

    你很可能是正确的,这个类的作者在构造函数中放入了不必要的操作。

    即使m_wheels 是堆分配类型,例如std::vector,仍然没有理由“清除”它,因为它已经传递给它的拥有移动构造器:

    : m_wheels(std::move(ori.m_wheels)),   // invokes move-constructor of appropriate type
    

    但是,您还没有展示足够多的类来让我们知道在这种特殊情况下是否需要显式的“清除”操作。根据 Deduplicator 的回答,以下函数应在“已移出”状态下正常运行:

    // Destruction
    ~Motorcycle(); // This is the REALLY important one, of course
    // Assignment
    operator=(const Motorcycle&);
    operator=(Motorcycle&&);
    

    因此,您必须查看这些函数中每个的实现,以确定移动构造函数是否正确。

    如果所有三个都使用默认实现(从您所展示的内容来看,这似乎是合理的),那么没有理由手动清除已移动的对象。但是,如果这些函数中的任何一个使用m_wheelsm_speedm_direction 的值来确定行为方式,则移动构造函数可能需要清除它们以确保正确的行为。

    这样的类设计不优雅且容易出错,因为通常我们不会期望原语的值在“移出”状态下很重要,除非原语被明确用作指示是否清理的标志必须出现在析构函数中。

    最终,作为在 C++ 类中使用的 示例,显示的移动构造函数在技术上并不是“错误”,因为它会导致不希望的行为,但它似乎是一个糟糕的示例,因为它很可能会引起您发布此问题的确切困惑。

    【讨论】:

      【解决方案4】:

      对于移出的对象,有几件事情必须安全地完成:

      1. 销毁它。
      2. 分配/移动到它。
      3. 任何其他明确说明的操作。

      因此,您应该尽可能快地摆脱它们,同时提供这些很少的保证,并且只有在它们有用并且您可以免费实现它们的情况下才能更多。
      这通常意味着不清除源头,并且其他时候意味着这样做。

      此外,为了简洁和保持琐碎性,更喜欢显式默认而不是用户定义的函数,以及隐式定义的函数。

      【讨论】:

      • 我可以编写一个类,在被移出后分配/移动到它不再安全。没有必须。所有标准类型都支持您的两个保证,并且标准可能假定类型在标准容器中作用于您的类型时支持这些保证,但这并不意味着必须
      • @Yakk 好吧,你总是可以打破规则,真的。不过这不是一个好主意。
      • 如何维护不变量?
      • @juanchopanza:只有那些与我列出的点相关的内容必须被维护,因为调用任何依赖于其余部分的内容都是非法的。
      【解决方案5】:

      对此没有标准行为。与指针一样,您在删除它们后仍然可以使用它们。有些人认为您不应该关心并且不重用该对象,但编译器不会禁止这样做。

      这是一篇关于此的博文(不是我写的),评论中有一些有趣的讨论:

      https://foonathan.github.io/blog/2016/07/23/move-safety.html

      以及后续:

      https://foonathan.github.io/blog/2016/08/24/move-default-ctor.html

      这里还有一个关于这个主题的最近视频聊天,其中有讨论这个问题的论点:

      https://www.youtube.com/watch?v=g5RnXRGar1w

      基本上,它是将移动对象视为已删除的指针,或者将其置于“已移动状态”的安全状态

      【讨论】:

        【解决方案6】:

        谁定义了不确定状态的含义?

        我认为是英语。 “不确定”在这里具有其通常的英语含义之一,"Not determinate; not precisely fixed in extent; indefinite; uncertain. Not established. Not settled or decided"。移出对象的状态不受标准的约束,除非它必须对该对象类型“有效”。每次移动对象时,它都不必相同。

        如果我们只讨论实现提供的类型,那么正确的语言应该是“有效但未指定的状态”。但我们保留“未指定”一词来讨论 C++ 实现的细节,而不是允许用户代码做什么的细节。

        “有效”是为每种类型单独定义的。以整数类型为例,陷阱表示是无效的,任何表示值的都是有效的。

        这里的标准并不是说移动构造函数必须使对象是不确定的,只是它不需要将其置于任何确定的状态。因此,尽管您说 0 并不比旧值“更不确定”是正确的,但无论如何它都没有实际意义,因为移动构造函数不需要使旧对象“尽可能地不确定”。

        在这种情况下,类的作者选择将旧对象置于一种特定状态。然后,他们是否记录该状态是什么完全取决于他们,如果他们这样做了,那么他们是否依赖它完全取决于代码的用户。

        我通常会建议不要依赖它,因为在某些情况下,您编写的代码认为它在语义上是一个移动,实际上会复制。例如,您将std::move 放在作业的右侧,而不关心对象是否为const,因为它可以使用任何一种方式,然后其他人出现并认为“啊,它已被移出,它必须被清零”。没有。他们忽略了Motorcycle 在移出时被清除,但const Motorcycle 当然不是,无论文档可能向他们建议什么:-)

        如果您要设置一个状态,那么这实际上是一个掷硬币的状态。您可以将其设置为“清除”状态,可能与无参数构造函数所做的相匹配(如果有的话),基于这代表最中性的值。而且我想在许多架构上,0 是(可能是联合)最便宜的设置值。或者,您可以将其设置为一些引人注目的值,希望当有人编写错误时,他们不小心从一个对象移动然后使用它的值,他们会想自己“什么?光速?在一个住宅街?哦,是的,我记得,这是这个班级在搬出时设定的价值,我可能是这样做的。

        【讨论】:

          【解决方案7】:

          源对象是一个右值,可能是一个 xvalue。因此,需要关注的是该对象即将被销毁。

          资源句柄或指针是区分移动和复制的最重要的项目:在操作之后,该句柄的所有权被假定转移。

          很明显,正如您在问题中提到的,在移动过程中,我们需要影响源对象,使其不再将自己标识为转移对象的所有者。

          Thing::Thing(Thing&& rhs)
               : unqptr_(move(rhs.unqptr_))
               , rawptr_(rhs.rawptr_)
               , ownsraw_(rhs.ownsraw_)
          {
              the.ownsraw_ = false;
          }
          

          请注意,我不清除rawptr_

          这里做了一个设计决定,只要所有权标志只对一个对象为真,我们不关心是否有一个悬空指针。

          但是,另一位工程师可能会决定应该清除指针,而不是随机 ub,以下错误会导致 nullptr 访问:

          void MyClass::f(Thing&& t) {
              t_ = move(t);
              // ...
              cout << t;
          }
          

          对于像问题中显示的变量这样无害的情况,它们可能不需要清除,但这取决于类设计。

          考虑:

          class Motorcycle
          {
              float speed_ = 0.;
              static size_t s_inMotion = 0;
          public:
              Motorcycle() = default;
              Motorcycle(Motorcycle&& rhs);
              Motorcycle& operator=(Motorcycle&& rhs);
          
              void setSpeed(float speed)
              {
                  if (speed && !speed_)
                      s_inMotion++;
                  else if (!speed && speed_)
                      s_inMotion--;
                  speed_ = speed;
              }
          
              ~Motorcycle()
              {
                  setSpeed(0);
              }
          };
          

          这是一个相当人为的证明,所有权不一定是简单的指针或布尔值,而可能是内部一致性问题。

          移动操作符可以使用setSpeed 来填充自己,也可以这样做

          Motorcycle::Motorcycle(Motorcycle&& rhs)
              : speed_(rhs.speed_)
          {
              rhs.speed_ = 0;  // without this, s_inMotion gets confused
          }
          

          (对错别字或自动更正表示歉意,在我的手机上输入)

          【讨论】:

            猜你喜欢
            • 2016-05-19
            • 2017-05-15
            • 2014-07-09
            • 2021-05-28
            • 2020-12-03
            • 2020-09-06
            • 1970-01-01
            • 2015-07-10
            • 2021-08-18
            相关资源
            最近更新 更多