0

背景

我今天早些时候阅读了以下答案,感觉就像是在重新学习 C++。

什么是移动语义?

什么是复制和交换成语?

然后我想知道我是否应该改变我的“方式”来使用这些令人兴奋的功能;我主要关心的是代码效率和清晰度(前者对我来说比后者更重要)。这导致我这篇文章:

为什么要有移动语义?

我强烈不同意(我同意答案,即);我不认为巧妙地使用指针会使移动语义变得多余,无论是在效率还是清晰度方面。

问题

目前,每当我实现一个重要的对象时,我大致这样做:

struct Y
{
    // Implement
    Y();
    void clear();
    Y& operator= ( const& Y );

    // Dependent
    ~Y() { clear(); }

    Y( const Y& that )
        : Y()
    {
        operator=(that);
    }

    // Y(Y&&): no need, use Y(const Y&)
    // Y& operator=(Y&&): no need, use Y& operator=(const Y&)
};

根据我从今天阅读的前两篇文章中了解到的情况,我想知道改成这个是否有益:

struct X
{
    // Implement
    X();
    X( const X& );

    void clear();
    void swap( X& );

    // Dependent
    ~X() { clear(); }

    X( X&& that )
        : X()
    {
        swap(that);
        // now: that <=> X()
        // and that.~X() will be called shortly
    }

    X& operator= ( X that ) // uses either X( X&& ) or X( const X& )
    { 
        swap(that); 
        return *this; 
        // now: that.~X() is called
    }

    // X& operator=(X&&): no need, use X& operator=(X)
};

现在,除了稍微复杂和冗长之外,我看不到第二个 ( struct X) 会产生性能改进的情况,而且我发现它的可读性也较差。假设我的第二个代码正确使用了移动语义,它将如何改进我当前的做事“方式”struct Y)?


注1:我认为使后者更清楚的唯一情况是“退出功能”

X foo()
{
    X automatic_var;
    // do things
    return automatic_var;
}
// ...
X obj( foo() );

我认为替代方法使用std::shared_ptrstd::reference_wrapper如果我厌倦了get()

std::shared_ptr<Y> foo()
{
    std::shared_ptr<Y> sptr( new Y() );
    // do things
    return sptr;
}
// ...
auto sptr = foo();
std::reference_wrapper<Y> ref( *ptr.get() );

只是稍微不太清楚,但同样有效。

注2:我确实努力使这个问题准确和可回答,并且不受讨论;仔细考虑,不要将其解释为“为什么移动语义有用”,这不是我要问的。

4

4 回答 4

1

目前,每当我实现一个重要的对象时,我都会大致这样做......

我相信你会在有更复杂的数据成员时放弃这一点 - 例如,在默认构造期间执行一些校准、数据生成、文件 I/O 或资源获取的类型,只会在(重新)分配时被丢弃/释放。

我看不到第二个 ( struct X) 会产生性能改进的情况。

那你还不了解移动语义。我向你保证,这种情况是存在的。但是鉴于“为什么移动语义有用”,这不是我要问的。我不会在您自己的代码上下文中再次向您解释它们......请自己“请仔细考虑”。如果您的想法再次失败,请尝试添加std::vector<>许多 MB 的数据和基准。

于 2015-02-04T02:31:07.703 回答
1

std::shared_ptr将您的数据存储在免费存储中(运行时开销),并具有线程安全的原子增量/减量(运行时开销),并且可以为空(忽略它并获取错误,或者不断检查它的运行时和程序员时间开销),并且有一个重要的预测对象的生命周期(程序员开销)。

它在任何方面、形状或形式上都不像move.

当 NRVO 和其他形式的省略失败时会发生移动,因此如果您使用对象作为值进行廉价移动,则意味着您可以依赖省略。没有便宜的举动,依赖省略是危险的:省略在实践中既脆弱又不受标准保证。

高效移动还可以使对象容器高效,而无需存储智能指针容器。

唯一指针解决了共享指针的一些问题,除了强制自由存储和可空性之外,它还阻碍了复制构造的易用性。

顺便说一句,您计划的可移动模式存在问题。

首先,您在移动构造之前不必要地默认构造。有时默认构造不是免费的。

其次operator=(X),由于标准中的一些缺陷,它不能很好地发挥作用。我忘记了为什么——组合或继承问题?-- 我会尽量记住回来编辑它。

如果默认构造几乎是免费的,并且交换是按元素进行的,那么这里有一个 C++14 方法:

struct X{
  auto as_tie(){
    return std::tie( /* data fields of X here with commas */ );
  }
  friend void swap(X& lhs, X& rhs){
    std::swap(lhs.as_tie(), rhs.as_tie());
  }
  X();// implement
  X(X const&o):X(){
    as_tie()=o.as_tie();
  }
  X(X&&o):X(){
    as_tie()=std::move(o.as_tie());
  }
  X&operator=(X&&o)&{// note: lvalue this only
    X tmp{std::move(o)};
    swap(*this,o);
    return *this;
  }
  X&operator=(X const&o)&{// note: lvalue this only
    X tmp{o};
    swap(*this,o);
    return *this;
  }
};

现在,如果您有需要手动复制的组件(例如 a unique_ptr),则上述内容不起作用。我只是value_ptr自己写一个(被告知如何复制)以使这些细节远离数据消费者。

as_tie函数还使==<(和相关的)易于编写。

如果X()是非平凡的,两者都X(X&&)可以X(X const&)手动编写并恢复效率。如此operator=(X&&)短,拥有其中两个也不错。

作为备选:

X& operator=(X&&o)&{
  as_tie()=std::move(o.as_tie());
  return *this;
}

是另一个=有其优点的实现(同样适用const&)。在某些情况下它可能更有效,但异常安全性更差。它还消除了对 的需要swap,但无论如何我都会留下swap:元素交换是值得的。

于 2015-02-04T03:39:43.760 回答
1

补充其他人所说的:

您不应该通过调用来实现构造函数operator=。你这样做:

Y( const Y& that ) : Y()
{
    operator=(that);
}

避免这种情况的原因是它需要默认构造 a (如果没有默认构造函数Y,它甚至不起作用);Y它还会毫无意义地创建和销毁默认构造函数可能分配的任何资源。

您通过使用 copy-and-swap 习语正确地解决了这个问题,X但随后您引入了一个类似的错误:

X( X&& that ) : X()
{
    swap(that);

移动构造函数应该构造,而不是交换。再次有一个毫无意义的默认构造,然后销毁默认构造函数可能已分配的任何资源。

您必须实际编写移动构造函数来移动每个成员。它必须是正确的,这样您的统一复制/移动分配才能正常工作。


更一般的评论:你应该很少做所有这些。这就是您为某些东西创建 RAII 包装器的方式;您更复杂的对象应该由兼容 RAII 的子对象组成,以便它们可以遵循零规则。在大多数情况下,都有预先存在的包装器,shared_ptr因此您不必编写自己的包装器。

于 2015-02-04T04:15:47.260 回答
0

一个显着的改进是当函数“使用”或以其他方式与参数一起工作时(即:按值而不是按引用)。在这种情况下,如果没有移动语义,传入一些左值作为参数将强制进行复制。

移动语义使调用者可以选择让函数复制值(尽管可能代价高昂)或将值“移动”到函数中,因为知道在此调用之后它不再使用传入的左值。

此外,如果一个函数使用 shared_ptr(或其他需要分配的包装器)只是为了能够将返回值移出,则使用移动语义可以摆脱该分配。因此,它不仅是一种精美,而且是一种性能提升。

虽然编写不使用移动语义并保持效率的代码很容易,但使用移动语义允许支持它们的类型的用户拥有更简单的接口和用例(基本上总是使用值语义)。

编辑:

由于 OP 特别提到了效率,因此值得补充的是,移动语义最多可以达到与没有它们时相同的效率。

据我所知,对于任何给定的代码片段,如果添加 std::move 会带来性能优势而不是没有它,只需重新编写代码就可以匹配移动的最佳效率甚至击败它。

我最近写的一段代码是一个生产者,它迭代了一些数据,选择并转换为消费者填充缓冲区。我写了一些通用的噱头,抽象了捕获数据并将其分流给消费者。

消费者要么是同一线程(即:在生产者通过管道处理时进行处理)、另一个线程(标准单线程消费者处理),要么是 PC 平台的多线程。

在这种情况下,生产者代码看起来相当简洁,因为它填充了一个容器,并通过上述每个平台定义的机制将其 std::move'd 到消费者中。

如果有人建议在不使用移动语义的情况下可以达到相同的效果,那将是完全正确的。它只是让我拥有 - 我认为 - 是更简单的代码。通常,我要支付的价格在对象的定义中更加笨拙,但在这种情况下,它是一个标准容器,所以其他人做了这项工作;)

于 2015-02-04T02:04:12.387 回答