问题 为什么互斥锁和条件变量可以轻易复制?


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

    …
  }
}

这不应该取消他们可以轻易复制的资格吗?


12826
2018-03-22 16:44


起源

看起来这只是霍华德在讨论中所说的,可能没有考虑过它? - SergeyA
你确定 std::mutex 是否可以复制?根据标准,他们复制构造函数,复制赋值都标记为 delete。 - NathanOliver
@NathanOliver这是LWG 1734的要点。(虽然你是对的 - mutex 不是轻易复制的。因为它的析构函数不是微不足道的。) - Columbo
@NathanOliver:删除的特殊成员函数在技术上仍然是一个微不足道的功能。 - Nicol Bolas
@NathanOliver海湾合作委员会似乎认为就是这样。 - Joseph Thomson


答案:


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

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

不使用 is_trivial 也不 is_trivially_copyable! EVER !!!

而是使用以下其中一个:

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_trivial 和 is_trivially_copyable。如果你确实碰巧查了一下,然后花了10分钟来分析它,它可能会也可能不会做你直觉认为它做的事情。如果您设法正确分析它,CWG很可能会在很少或没有通知的情况下更改其定义并使您的代码无效。

运用 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>

完全按照他们的意思行事,并且不太可能改变他们的定义。必须单独处理每个特殊成员似乎过于冗长。但它会为您的代码的稳定性/可靠性带来回报。如果必须,将这些个性特征打包成自定义特征。

更新

例如,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


7
2018-03-22 20:41



从马的嘴里!感谢您的建议。我没有意识到这些特征是在C ++ 17中。为了清楚起见,您认为标准是强制要求用户提供的析构函数 std::mutex 和公司?我问,因为其他答案似乎并不认为是这种情况。 - Joseph Thomson
是的,但这是标准中的模糊区域。互斥规范并没有考虑到关于“琐碎”的规则。这种特质可能会改变。但总的来说,我并不认为规范中关于互斥体是否可以轻易复制的重要性,因为“平凡可复制”的定义既奇特又容易改变。更重要的是这样的问题:mutex可以简单地复制构造吗? mutex可以简单地复制分配吗?即使答案不可移植,后面的这些问题也可以被轻易询问并清楚地回答,从而产生可移植的代码。 - Howard Hinnant
到底是什么 is_trivially_copy_constructible 对...有用?它本身不保证按字节顺序复制。事实上,建议 is_trivially_copy_constructible 代替 is_trivially_copyable 可能是危险的,因为人们可以(虚假地)推断出这一点 memcpy如果复制构造函数很简单,那就很好。 - Columbo
@Columbo:我试图用一个例子来澄清。 - Howard Hinnant
我喜欢用 test() 作为在类中定义的私有静态成员函数(我称之为 assert_traits() 通常),这样我就可以在定义的角度看待类语义。该 static_asserts 因为一个类在成员定义中被认为是完整的。 - TemplateRex


并非所有实现都提供了一个非常重要的析构函数 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 是标准的符合和平凡的可复制(可以验证 通过Coliru)。同样,没有什么可以阻止实现保持 condition_variable 平凡的可破坏的(参见 [thread.condition.condvar] / 6,虽然我找不到一个实现)。

最重要的是,我们需要明确的,规范的保证,而不是对内容的巧妙,微妙的解释 condition_variable 做或不做(以及如何做到这一点)。


4
2018-03-22 17:08



我在另一个答案中问了同样的问题,但是不存在 ~mutex(); 在标准中指定析构函数不应该是默认的,或者这只是类接口可能是什么样子的一个例子? - Joseph Thomson
编辑了问题以证明我可能存在误解的根源。 - Joseph Thomson
@JosephThomson不,这并不暗示。看到 [functions.within.classes] / 1。 - Columbo


从语言律师的角度来看这一点非常重要。

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

但这没关系。为什么?因为标准没有明确声明这些类型不会轻易复制。因此,从标准的角度来看, 理论上可行 使这些对象可以轻易复制。

尽管每个都没有功能实现,但N4460的要点是要明确这些类型永远不会轻易复制。


3
2018-03-22 16:54



我不确定他们是否真的在这里采取了正确的方法。在我看来,正确的方法是将删除的赋值运算符/复制ctors视为类型 非 - 可复制的。是的,它打破了一些向后兼容性,但它是 正确的事。如果无法复制某些内容 operator=,为什么复制合法是合法的 memcpy()? - SergeyA
但是,标准是否为这些类型指定了用户提供的析构函数? - Joseph Thomson
@JosephThomson:它指定有一个析构函数;类型必须是 Destructibe。但是标准中没有任何内容要求析构函数是非平凡的。再一次,像语言律师一样思考; “这不是没有规则”,所以这是可能的。 - Nicol Bolas
@SergeyA:“是的,它打破了一些向后兼容性“就委员会而言,这就是它的结束。关于违约/删除成员函数的简单可复制规则和各种规则并不是要改变现有行为。 - Nicol Bolas
@JosephThomson,没有'TriviallyMovable'这样的东西。因此,要分析可移动对象,需要检查琐碎的移动构造函数。 - SergeyA