为什么互斥体和条件变量很容易复制

Why are mutexes and condition variables trivially copyable?

本文关键字:变量 很容易 复制 条件 为什么      更新时间:2023-10-16

LWG 2424 讨论了原子组学、互斥体和条件变量在 C++14 中可简单复制的不良状态。我很欣赏已经排好了修复程序,但std::mutexstd::condition variable等人似乎有非平凡的析构函数。例如:

30.4.1.2.1 类互斥锁 [thread.mutex.class]

namespace std {
  class mutex {
  public:
    constexpr mutex() noexcept;
    ~mutex(); // user-provided => non-trivial
    …
  }
}

这难道不应该取消他们作为微不足道的可复制的资格吗?

要么

是我的错误,要么是我被错误引用了,老实说我不记得是哪个。

但是,我对这个问题有非常强烈的建议:

不要使用

is_trivial,也不要使用is_trivially_copyable! 曾!!!

请改用以下方法之一:

is_trivially_destructible<T>
is_trivially_default_constructible<T>
is_trivially_copy_constructible<T>
is_trivially_copy_assignable<T>
is_trivially_move_constructible<T>
is_trivially_move_assignable<T>

理由:

TLDR:看到这个很好的问题和正确的答案。

没有人(包括我自己)能记住is_trivialis_trivially_copyable的定义. 如果你碰巧查了一下,然后花10分钟分析它,它可能会也可能不会做你直觉认为它做的事情。 如果您设法正确分析它,CWG 很可能会在很少或没有通知的情况下更改其定义并使您的代码无效。

使用is_trivialis_trivially_copyable就是在玩火。

但是这些:

is_trivially_destructible<T>
is_trivially_default_constructible<T>
is_trivially_copy_constructible<T>
is_trivially_copy_assignable<T>
is_trivially_move_constructible<T>
is_trivially_move_assignable<T>

完全按照他们听起来的样子去做,并且不太可能改变他们的定义。 不得不单独处理每个特殊成员似乎过于冗长。 但它会在代码的稳定性/可靠性方面得到回报。 如果必须,请将这些个人特征打包成自定义特征。

更新

例如,clang & gcc 编译了这个程序:

#include <type_traits>
template <class T>
void
test()
{
    using namespace std;
    static_assert(!is_trivial<T>{}, "");
    static_assert( is_trivially_copyable<T>{}, "");
    static_assert( is_trivially_destructible<T>{}, "");
    static_assert( is_destructible<T>{}, "");
    static_assert(!is_trivially_default_constructible<T>{}, "");
    static_assert(!is_trivially_copy_constructible<T>{}, "");
    static_assert( is_trivially_copy_assignable<T>{}, "");
    static_assert(!is_trivially_move_constructible<T>{}, "");
    static_assert( is_trivially_move_assignable<T>{}, "");
}
struct X
{
    X(const X&) = delete;
};
int
main()
{
    test<X>();
}

请注意,X可复制的,但不能简单复制可构造。 据我所知,这是顺从行为。

VS-2015目前表示X既不能轻易复制,也不能轻易复制可构造。 根据当前的规范,我相信这是错误的,但它肯定符合我的常识告诉我的。

如果我需要memcpy未初始化的内存,我会相信is_trivially_copy_constructible而不是is_trivially_copyable向我保证这样的操作是可以的。 如果我想memcpy初始化内存,我会检查is_trivially_copy_assignable

并非所有实现都为 mutex 提供非平凡析构函数。 参见libstdc++(并假设已经定义了__GTHREAD_MUTEX_INIT):

  // Common base class for std::mutex and std::timed_mutex
  class __mutex_base
  {
  // […]
#ifdef __GTHREAD_MUTEX_INIT
    __native_type  _M_mutex = __GTHREAD_MUTEX_INIT;
    constexpr __mutex_base() noexcept = default;
#else
  // […]
    ~__mutex_base() noexcept { __gthread_mutex_destroy(&_M_mutex); }
#endif
  // […]
  };
  /// The standard mutex type.
  class mutex : private __mutex_base
  {
    // […]
    mutex() noexcept = default;
    ~mutex() = default;
    mutex(const mutex&) = delete;
    mutex& operator=(const mutex&) = delete;
  };

mutex的这种实现既符合标准又易于复制(可以通过 Corilu 进行验证)。同样,没有什么能阻止实现保持condition_variable微不足道的可破坏性(参见 [thread.condition.condvar]/6,尽管我找不到这样做的实现)。

底线是,我们需要明确的、规范性的保证,而不是对condition_variable做什么或不需要做什么(以及它必须如何做到这一点)进行聪明、微妙的解释。

从语言律师的角度来看待这个问题很重要。

实现基本上不可能实现mutex、条件变量等,使它们易于复制。在某些时候,你必须编写一个析构函数,而这个析构函数很可能必须做一些不平凡的工作。

但这并不重要。为什么?因为该标准没有明确说明此类类型不会是微不足道的可复制的。因此,从标准的角度来看,理论上这些对象是可以轻易复制的。

尽管没有功能实现,但N4460的重点是非常清楚地表明,此类类型永远不会被简单地复制。