【问题标题】:Should I std::move a shared_ptr in a move constructor?我应该在移动构造函数中 std::move 一个 shared_ptr 吗?
【发布时间】:2012-06-12 18:39:25
【问题描述】:

考虑:

#include <cstdlib>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

class Gizmo
{
public:
    Gizmo() : foo_(shared_ptr<string>(new string("bar"))) {};
    Gizmo(Gizmo&& rhs); // Implemented Below

private:
    shared_ptr<string> foo_;
};

/*
// doesn't use std::move
Gizmo::Gizmo(Gizmo&& rhs)
:   foo_(rhs.foo_)
{
}
*/


// Does use std::move
Gizmo::Gizmo(Gizmo&& rhs)
:   foo_(std::move(rhs.foo_))
{
}

int main()
{
    typedef vector<Gizmo> Gizmos;
    Gizmos gizmos;
    generate_n(back_inserter(gizmos), 10000, []() -> Gizmo
    {
        Gizmo ret;
        return ret;
    });

    random_shuffle(gizmos.begin(), gizmos.end());

}

在上面的代码中,Gizmo::Gizmo(Gizmo&amp;&amp;) 有两个版本——一个使用std::move 来实际移动shared_ptr,另一个只是复制shared_ptr

这两个版本似乎都在表面上起作用。一个区别(我能看到的唯一区别)是在非move 版本中,shared_ptr 的引用计数暂时增加,但只是短暂增加。

我通常会继续使用move shared_ptr,但只是为了在我的代码中保持清晰和一致。我在这里错过了一个考虑吗?出于任何技术原因,我应该更喜欢一个版本吗?

【问题讨论】:

  • 在移动构造函数中移动至少在语义上是一致的......
  • 为什么要将字符串保存在 shared_ptr 中? shared_ptr 作为成员变量通常是糟糕设计的标志。
  • 在移动构造函数中移动符合编译器自动生成的内容。
  • @ViktorSehr:“shared_ptr 作为成员变量通常是糟糕设计的标志。”你为什么这么认为?如果您的对象与另一个对象共享一个对象的所有权,那么拥有 shared_ptr 数据成员并没有错...
  • @ViktorSehr:如果将 shared_ptr 放入成员变量中是不好的设计……您还会 把它放在哪里?如果对象不能共享某物的所有权,那么共享所有权有多大用处?

标签: c++ c++11 shared-ptr rvalue-reference


【解决方案1】:

这里的主要问题不是由于shared_ptr 中额外的原子递增和递减导致的性能差异很小,而是除非您执行移动,否则操作的语义不一致。

虽然假设shared_ptr 的引用计数只是暂时的,但语言中没有这样的保证。您要从中移动的源对象可以是临时的,但它也可能具有更长的生命周期。它可能是一个已被强制转换为 rvalue-reference 的命名变量(比如std::move(var)),在这种情况下,通过不从shared_ptr 移动 你仍然与移动源保持共享所有权,如果目标shared_ptr 的范围更小,则指向对象的生命周期将不必要地延长。

【讨论】:

  • 我想知道在使用招式时,我们应该在多大程度上思考“这个招式可能会降级为副本”?显然,我们在为任意MoveConstructible 类型T 编写模板代码时会这样做,因为它实际上根本不需要移动构造函数,更不用说修改源代码的构造函数了。同样显然,如果移动对象不必要地持有资源,那么 QoI 很差,所以如果 Gizmo 确实有移动构造函数,那么它应该是一个很好的构造函数。但我认为这是一个约定和编码风格的问题,当它不是时,我们有资格成为什么样的人。
  • +1,这就是我在评论詹姆斯回答时所要表达的意思。说得好。
  • @SteveJessop:毫无疑问,交换或复制是move的有效实现,但它们不符合最不意外的原则.
  • @SteveJessop:如果内存是由类型管理的,那么令人惊讶的是源对象中的char* 没有被清除。一般来说,movable类型会通过指针来持有资源,moving会复制指针然后将源重置为0放弃所有权...
  • @David:我只是指出,在具有严格指针安全性的实现中,原始指针可能会实现共享所有权(例如通过标记清除垃圾收集器)。此外,由于标准中对 MoveConstructible 和 MoveAssignable 的定义方式,“可移动”一词可能是模棱两可的。它可能意味着“实际上可以移动”,也可能意味着“可以移动或复制”。因此,由于具有不同含义的潜力,不同的人会对不同的事物感到惊讶。
【解决方案2】:

我赞成 James McNellis 的回答。我想对他的回答发表评论,但我的评论不符合评论格式。所以我把它放在这里。

衡量移动shared_ptr 与复制一个shared_ptr 对性能影响的一种有趣方法是使用vector&lt;shared_ptr&lt;T&gt;&gt; 之类的东西来移动或复制一大堆它们并计时。大多数编译器都可以通过指定语言模式(例如 -std=c++03 或 -std=c++11)来打开/关闭移动语义。

这是我刚刚在 -O3 测试的代码:

#include <chrono>
#include <memory>
#include <vector>
#include <iostream>

int main()
{
    std::vector<std::shared_ptr<int> > v(10000, std::shared_ptr<int>(new int(3)));
    typedef std::chrono::high_resolution_clock Clock;
    typedef Clock::time_point time_point;
    typedef std::chrono::duration<double, std::micro> us;
    time_point t0 = Clock::now();
    v.erase(v.begin());
    time_point t1 = Clock::now();
    std::cout << us(t1-t0).count() << "\u00B5s\n";
}

使用 clang/libc++ 并在 -std=c++03 中为我打印出来:

195.368µs

切换到 -std=c++11 我得到:

16.422µs

您的里程可能会有所不同。

【讨论】:

  • +1:只是观察。当我调整它以使用上面的Gizmo 类时,move 和非move 版本的时间几乎相同。这是在 MSVC10 上,使用 boost::chrono 而不是 std::chrono,编译 x64 版本。
  • @JohnDibling:很有趣。如果你知道为什么我们的结果会有如此大的差异,我很想听听。要尝试的一件事:在移动构造函数上放置一个 noexcept 。我不知道 MSVC10 是否实现了这一点。如果考虑到 noexcept 来得太晚,我会感到惊讶。而且我实际上不希望这会对 vector::erase 成员产生影响。但无论如何,这是我要尝试的第一件事。我在 2.8 GHz Intel Core i5(编译为 64 位)上运行。你的结果是几百微秒,还是几十微秒?
【解决方案3】:

使用move 更可取:它应该比副本更有效,因为它不需要引用计数的额外原子递增和递减。

【讨论】:

  • 语义上也存在差异:人们会期望从Gizmo 移出的Gizmo 保持其内部共享状态处于活动状态吗?就我个人而言,我会觉得这很令人惊讶,并期望一个已移动的对象释放所有共享状态。
  • @ildjarn:我同意,尽管它不会影响程序的正确性:移动的对象仍然是可破坏的和可分配的。
猜你喜欢
  • 1970-01-01
  • 2013-08-08
  • 2015-02-14
  • 2014-03-13
  • 2013-01-22
  • 2017-06-11
  • 2013-01-24
  • 2013-12-08
  • 1970-01-01
相关资源
最近更新 更多