【问题标题】:Why are mutexes and condition variables trivially copyable?为什么互斥锁和条件变量可以轻松复制?
【发布时间】:2016-03-22 16:44:32
【问题描述】:

LWG 2424 在 C++14 中将原子、互斥体和条件变量的不良状态讨论为 trivially copyable。我很欣赏修复是already lined up,但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

    …
  }
}

这难道不应该取消它们的可复制性吗?

【问题讨论】:

  • 看起来这只是霍华德在讨论中说的话,可能没有经过深思熟虑?
  • 您确定std::mutex 可以轻松复制吗?根据标准,他们复制构造函数和复制赋值都标记为delete
  • @NathanOliver 这就是 LWG 1734 的意义所在。(尽管您是对的 - mutex 并非可简单复制。因为它的析构函数并非微不足道。)
  • @NathanOliver:删除的特殊成员函数在技术上仍然是微不足道的。
  • @NathanOliver GCC 似乎认为就是这样。

标签: c++ language-lawyer c++14


【解决方案1】:

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

但是,我对此主题有以下非常强烈的建议:

请勿使用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>

理由:

tldr:看到这个excellent question and correct answer.

没有人(包括我自己)能记住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

【讨论】:

  • 从马的嘴里!谢谢你的建议。我没有意识到这些特征在 C++17 中。为了清楚起见,您认为标准是在 std::mutex 和公司的情况下强制用户提供析构函数?我问是因为其他答案似乎不认为是这种情况。
  • 是的,但这是标准中的一个模糊区域。互斥量规范的设计并未考虑到“琐碎”的规则。这样的特征可能会改变。但总而言之,我认为规范中关于互斥锁是否可简单复制的内容并不重要,因为“可简单复制”的定义既奇怪又可能会发生变化。更重要的是这样的问题:互斥量可以简单地复制构造吗?可以简单地复制分配互斥锁吗?即使答案不可移植,后面的这些问题也可以便携地提出并清楚地回答,从而产生可移植的代码。
  • is_trivially_copy_constructible 到底有什么用处?它本身并不保证按字节复制。事实上,建议 is_trivially_copy_constructible 而不是 is_trivially_copyable 可能是危险的,因为如果复制构造函数是微不足道的,人们可能(虚假地)推断 memcpy'ing 很好。
  • @Columbo:我试图用一个例子来说明。
  • 我喜欢使用test() 作为在类中定义的私有静态成员函数(我通常称之为assert_traits()),这样我就可以在定义点查看类语义。 static_asserts 起作用是因为一个类在成员定义中被认为是完整的。
【解决方案2】:

并非所有实现都为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 的此实现既符合标准又可轻松复制(可以验证 via Coliru)。同样,没有什么能阻止实现使condition_variable 易于破坏(参见[thread.condition.condvar]/6,尽管我找不到这样的实现)。

底线是,我们需要明确、规范的保证,而不是对 condition_variable 做什么或不必须做什么(以及它必须如何完成)做巧妙、微妙的解释。

【讨论】:

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

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

基本上不可能实现mutex、条件变量等以使它们易于复制的方式。在某些时候,您必须编写一个析构函数,而该析构函数很可能需要做一些重要的工作。

但这没关系。为什么?因为该标准没有明确声明此类类型不可轻易复制。因此,从标准的角度来看,理论上可能这样的对象可以简单地复制。

即使没有任何功能实现都可以,但 N4460 的重点是非常清楚地表明此类类型永远不会被轻易复制。

【讨论】:

  • 我不确定他们是否真的在这里采取了正确的方法。在我看来,正确的方法是将具有已删除的赋值运算符/复制 ctor 的类型视为-普通可复制。是的,它破坏了一些向后兼容性,但这是正确的。如果operator=不能复制,为什么memcpy()复制是合法的?
  • 但是标准没有为这些类型指定用户提供的析构函数吗?
  • @JosephThomson:指定有析构函数;类型必须是Destructibe。但是标准中没有任何内容要求析构函数是非平凡的。再次,像语言律师一样思考; “没有规则”,所以这是可能的。
  • @SergeyA:“是的,它破坏了一些向后兼容性” 就委员会而言,这就是它的结束。可简单复制的规则以及围绕默认/删除成员函数的各种规则并非旨在改变现有行为。
  • @JosephThomson,没有“TriviallyMovable”这样的东西。因此,要分析可移动对象,需要检查微不足道的移动构造函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-06
  • 2011-02-15
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多