(为什么)移动构造函数或移动赋值运算符应该清除它的参数?

时间:2016-09-09 14:06:54

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

从我正在采用的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<Wheel, 2>类型的成员,滚轮仅包含double m_speedbool m_rotating。在中摩托车课程,m_speedm_direction也是double。)

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

如果Motorcycle有任何指针成员,我们想要“偷”,那么我们必须设置ori.m_thingy = nullptr,以便不会,例如delete m_thingy两次。但是当字段包含对象本身时,它是否重要?

我向朋友询问了这个问题,他们向我指出this page,其中说:

  

通常移动构造函数&#34;窃取&#34;参数持有的资源(例如指向动态分配的对象,文件描述符,TCP套接字,I / O流,运行线程等),而不是复制它们,并将参数保留在某些有效但是否则不确定状态。例如,从std::stringstd::vector移动可能会导致参数保留为空。但是,不应该依赖这种行为。

谁定义不确定状态是什么意思?我没有看到如何将速度设置为0.0 不确定而不是保留它。就像报价中的最后一句所说的那样 - 代码不应该依赖于移动的摩托车的状态,那么为什么还要打扰它呢?

7 个答案:

答案 0 :(得分:30)

他们不需要进行清洁。该类的设计者决定将移动的对象初始化为零是个好主意。

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

答案 1 :(得分:13)

  

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

班上的作者。

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

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

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

答案 2 :(得分:10)

你很可能认为这个类的作者在构造函数中放了不必要的操作。

即使m_wheels是堆分配类型,例如std::vector,仍然没有理由&#34;清除&#34;它,因为已经传递给它的自己的移动构造函数:

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

但是,您没有展示足够的课程以允许我们知道是否明确&#34;清除&#34;在这种特殊情况下,操作是必要的。根据Deduplicator的回答,以下功能应该在&#34;移动来自&#34;状态:

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

因此,您必须查看这些函数的每个的实现,以确定move-constructor是否正确。

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

这样的类设计将是不优雅且容易出错的,因为通常我们不会期望基元的值在&#34;移动到&#34; state,除非将原语明确用作标志,以指示是否必须在析构函数中进行清理。

最终,作为在C ++类中使用的示例,所显示的move-constructor在技术上并不是错误的&#34;在导致不良行为的意义上,但它似乎是一个糟糕的例子,因为它可能会引起你发布这个问题的混乱。

答案 3 :(得分:3)

对于移动的对象,几乎没有必要安全可行的东西:

  1. 破坏它。
  2. 分配/移动到它。
  3. 明确说明的任何其他操作。
  4. 所以,你应该在给予这两个保证的同时尽可能快地离开它们,只有在它们有用且你可以免费实现它们时才更多。 通常意味着不清除来源,有时则意味着要这样做。

    作为补充,为了简洁和保留琐碎性,首选明确默认为用户定义的函数,并隐式定义两者之间的函数。

答案 4 :(得分:2)

这没有标准行为。与指针一样,删除它们后仍可以使用它们。有些人看到你不应该关心,只是不重用该对象,但编译器不会禁止它。

以下是一篇博文(不是我)关于这一点,并在评论中进行了一些有趣的讨论:

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

基本上它是关于处理移动的对象,如删除的指针,或者将其置于安全状态,以便“从状态移动”

答案 5 :(得分:2)

  

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

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

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

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

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

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

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

如果你要设置一个状态,那么它实际上是一个抛硬币状态。您可以将其设置为“清除”状态,可能与no-args构造函数的作用(如果有的话)相匹配,因为它代表了最中性的值。我想在许多架构上,0是(可能是联合的)最便宜的值来设置。或者你可以把它设置为一些引人注目的价值,希望当有人写一个错误,他们不小心从一个物体移动然后使用它的价值时,他们会想到自己“什么?光速?在一个住宅街吗?哦,是的,我记得,这是班级搬家时的价值,我可能就是这样做的。“

答案 6 :(得分:1)

源对象是一个右值,可能是一个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
}

(道歉或自动纠正的道歉,在我的手机上输入)