【问题标题】:C++11: write move constructor with atomic<bool> member?C++11:用 atomic<bool> 成员编写移动构造函数?
【发布时间】:2012-12-20 08:58:44
【问题描述】:

我有一个带有原子成员变量的类:

struct Foo
{
  std::atomic<bool> bar;
  /* ... lots of other stuff, not relevant here ... */
  Foo() 
  : bar( false )
  {}

  /* Trivial implementation fails in gcc 4.7 with:
   *   error: use of deleted function ‘std::atomic<bool>::atomic(const td::atomic<bool>&)’
   */
  Foo( Foo&& other )
  : bar( other.bar )
  {}
};

Foo f;
Foo f2(std::move(f));  // use the move

移动构造函数应该是什么样子的?

Gcc 4.7 不喜欢我的任何尝试(比如在 other.bar 周围添加 std::move()),而且这里的网络出奇地安静......

【问题讨论】:

    标签: c++ c++11 atomic move-semantics libstdc++


    【解决方案1】:

    std::atomic 不可复制或移动,因为its copy constructor is deleted 并且未定义移动构造函数。您必须显式加载另一个值并使用它来构造新值,正如 gustaf 的回答中所指出的那样。

    为什么std::atomic 不能移动?由于它是一个同步原语,所有线程必须同步相同的数据(即相同的地址)。当你复制(或移动)一个原子值时,你必须使用一些通信协议。它可能很简单,就像在您的示例中一样(只需加载它并使用它来初始化新的原子),但总的来说,我认为 C++11 强制您考虑它是一个很好的设计决策。否则,可能会导致代码看起来不错,但存在一些细微的同步问题。

    【讨论】:

    • 我正在尝试找出一种情况,当您有多个线程时,自动移动原子实际上可能会出现问题。使用 std::move() 手动强制移动是另一回事。无论哪种方式,我都想要一个例子来说明移动原子在哪里会出现问题。您可以移动任何其他 std 容器,并且您需要负责同步。
    • 我也很想知道如果atomic 有一个必须通过显式调用std::move() 来触发的移动构造函数是否是个坏主意——它会给我一个我所期望的界面。但我离成为 C++ 专家还差得太远,无法判断关于标准的决定是有目的还是错误......
    • 移动构造函数无法确定右值是由显式 std::move 产生还是隐式右值(例如,函数的返回值)。我也不是设计决策方面的专家,但我认为引用计数资源是移动构造函数可能导致问题的一个示例。我将编辑我的答案,并包含我在撰写有关“同步问题”时想到的示例。
    • 我又想了一遍,但坦率地说,我想不出任何你想移动原子的例子。即使是 ref-counted 示例也存在缺陷,因为根本不需要像移动这样的操作。所以我得出的结论是你从不想要在逻辑上移动它,尽管有时你想要复制它。然而,复制比您最初想象的要复杂得多。您必须加载该值(这可能涉及缓存同步),然后您必须分配它(即另一个可能的同步)。话虽如此,我认为明确要求load 的方法很好。
    【解决方案2】:

    由于您要移动other,因此其他人将无法访问它。所以从它的bar 读取是安全的,不管它是不是原子的。

    atomic&lt;T&gt; 只有两个构造函数,一个是默认的(),另一个是(T)。因此,您的代码看起来应该可以编译。如果没有,如果您将 other.bar 静态转换为 T,强制使用 (T) 构造函数会发生什么?

    : bar( static_cast&lt; bool &gt;( other.bar ) )

    或等于,也许不那么丑:

    : bar( other.bar.load( ) )

    【讨论】:

    • 谢谢,bar( other.bar.load() ) 是现在正在编译的正确解决方案!
    • 所以,您的代码看起来应该可以编译了。 不,atomic&lt;T&gt; 有一个已删除的复制构造函数,重载决议发现不是 atomic(T) 构造函数。演员或负载是必要的。
    • @JonathanWakely 你是对的,我认为当编译器知道复制构造函数被删除并且有一个operator T 时,它会继续下一个可能的构造函数(T)。这样做的原因是(T) 的原子构造函数是constexpr 使其行为类似于explicit 构造函数,因为隐式转换必须是显式的(或者在这种情况下是constexpr)?
    • 不,原因是调用的最佳匹配是复制构造函数,因此重载决策选择了它。删除的函数表示“忽略此函数”,而是表示“如果调用此函数,您的程序将无法编译”
    • 这在 Foo 有其他成员变量时不起作用。必须在用户定义的移动构造函数中煞费苦心地移动它们中的每一个。我多次遇到这种情况,因此决定尽可能避免使用原子,而是使用互斥锁。
    【解决方案3】:

    atomic&lt;bool&gt; 的模板实例化本质上是这样的:

    struct atomic<bool>
    {
        atomic<bool>(bool);
        atomic<bool>( const atomic<bool>& ) = delete;
        operator bool() const;
    }
    

    所以当你尝试复制它时:

    atomic<bool> a = ...;
    atomic<bool> b(a);
    

    选择了删除的复制构造函数并导致编译错误。

    您需要显式转换为 bool 才能通过 operator bool() --&gt; atomic&lt;bool&gt;(bool)...

    atomic<bool> a = ...;
    atomic<bool> b(bool(a));
    

    【讨论】:

    • 使用 static_cast 不是更安全吗(参考您解决方案中的最后一行代码)?
    • @GuyAvraham 因为新的命名转换比旧语法更清晰,所以更受欢迎。在这种情况下,.load() 会带来最大的清晰度。
    猜你喜欢
    • 1970-01-01
    • 2018-01-23
    • 2012-05-11
    • 1970-01-01
    • 2014-02-22
    • 2014-11-08
    • 2012-10-17
    相关资源
    最近更新 更多