移动语义如何改善"我的方式"?

时间:2015-02-04 01:45:02

标签: c++ c++11 move-semantics copy-and-swap

背景

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

What are move semantics?

What is the copy-and-swap idiom?

然后我想知道我是否应该改变使用这些令人兴奋的功能的“方式”;我所关注的主要问题是代码效率和清晰度(前者比后者稍微重要一点)。这导致我发表这篇文章:

Why have move semantics?

我非常不同意(我同意答案,即是);我认为智能使用指针不会使移动语义变得多余,无论是效率还是清晰度。

问题

目前,每当我实现一个非平凡的对象时,我大致会这样做:

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_ptr

,我认为使用std::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 个答案:

答案 0 :(得分:1)

  

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

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

  

我没有看到第二种情况(struct X)会带来性能提升的情况。

然后你还不了解移动语义。我向你保证这种情况存在。但鉴于&#39;&#34;为什么移动语义有用&#34;,这不是我要问的。&#39; 我不打算解释它们再次在你自己的代码的上下文中...去&#34;请自己考虑#34; 。如果再次考虑失败,请尝试在许多MB数据和基准测试中添加std::vector<>

答案 1 :(得分:1)

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

它不像move那样廉价,形状或形式。

当NRVO和其他形式的elision失败时发生移动,所以如果你有一个廉价的移动使用对象作为值意味着你可以依赖于elision。如果没有廉价的举动,依靠elision是危险的:elision在实践中既脆弱又不受标准保证。

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

一个独特的指针解决了共享指针的一些问题,除了强制自由存储和可空性,它还可以阻止复制构造的使用。

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

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

第二个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;
  }
};

现在如果您有需要手动复制的组件(如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:元素交换是值得的。

答案 2 :(得分:1)

添加其他人所说的话:

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

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

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

您通过使用复制和交换习惯用法正确地为X修复了此问题,但是您引入了类似的错误:

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

move-constructor应该构造,而不是交换。同样,默认构造函数可能已经分配了一个毫无意义的默认构造,然后销毁了所有资源。

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


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

答案 3 :(得分:0)

一个值得注意的改进是函数'消耗'或以其他方式使用参数(即:取值而不是参考)。在这种情况下,如果没有移动语义,将一些左值作为参数传入会强制复制。

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

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

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

编辑:

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

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

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

消费者要么是同一个线程(即:作为生产者通过管道处理),另一个线程(标准的单线程消费者交易),或PC平台的多线程。

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

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