可变数据成员、模板构造函数和普通复制构造函数

Han*_*Han 6 c++ mutable copy-constructor language-lawyer trivially-copyable

示例代码可以在下面或godbolt上找到。假设我们有 4 个班级:

  1. S<T>:持有数据成员。

  2. SCtor<T>:持有数据成员并具有模板构造函数。

  3. SCtorMutable<T>:持有可变数据成员并具有模板构造函数。

  4. SCtorDefault<T>:持有一个成员,有一个模板构造函数,有默认的复制/移动构造函数和默认的复制/移动赋值运算符。

所有编译器都同意这 4 个类是可以简单复制的。

如果有一个简单的包装类W<T>将上述任何一个类作为数据成员。包装类W<S...<T>>仍然是可简单复制的。

如果有另一个包装类WMutable<T>将上述类中的任何一个保存为可变数据成员。

  1. MSVC 仍然认为WMutable<S...<T>>是可以简单复制的。
  2. clang 认为WMutable<S<T>>是可以复制的。WMutable<SCtor...<T>>不是可简单复制构造的,因此也不是可简单复制的。
  3. gcc 认为WMutable<S<T>>是可以简单复制的。WMutable<SCtor...<T>>不是可简单复制构造的,而是可简单复制的。

应该WMutable<T>是可以简单复制的吗?

#include <type_traits>
#include <utility>

template<typename T>
struct S {
    T m_t;
};

template<typename T>
struct SCtor {
    T m_t;
    template<typename... U>
    SCtor(U&&... u): m_t(std::forward<U>(u)...) {}
};

template<typename T>
struct SCtorMutable {
    mutable T m_t;
    template<typename... U>
    SCtorMutable(U&&... u): m_t(std::forward<U>(u)...) {}
};

template<typename T>
struct SCtorDefault {
    T m_t;
    template<typename... U>
    SCtorDefault(U&&... u): m_t(std::forward<U>(u)...) {}
    SCtorDefault(SCtorDefault const&) = default;
    SCtorDefault(SCtorDefault&&) = default;
    SCtorDefault& operator=(SCtorDefault const&) = default;
    SCtorDefault& operator=(SCtorDefault&&) = default;
};

template<typename T>
struct W {
    T m_t;
};

template<typename T>
struct WMutable {
    mutable T m_t;
};

static_assert(std::is_trivially_copyable<S<int>>::value);
static_assert(std::is_trivially_copy_constructible<S<int>>::value);
static_assert(std::is_trivially_move_constructible<S<int>>::value);
static_assert(std::is_trivially_copy_assignable<S<int>>::value);
static_assert(std::is_trivially_move_assignable<S<int>>::value);

static_assert(std::is_trivially_copyable<SCtor<int>>::value);
static_assert(std::is_trivially_copy_constructible<SCtor<int>>::value);
static_assert(std::is_trivially_move_constructible<SCtor<int>>::value);
static_assert(std::is_trivially_copy_assignable<SCtor<int>>::value);
static_assert(std::is_trivially_move_assignable<SCtor<int>>::value);

static_assert(std::is_trivially_copyable<SCtorMutable<int>>::value);
static_assert(std::is_trivially_copy_constructible<SCtorMutable<int>>::value);
static_assert(std::is_trivially_move_constructible<SCtorMutable<int>>::value);
static_assert(std::is_trivially_copy_assignable<SCtorMutable<int>>::value);
static_assert(std::is_trivially_move_assignable<SCtorMutable<int>>::value);

static_assert(std::is_trivially_copyable<SCtorDefault<int>>::value);
static_assert(std::is_trivially_copy_constructible<SCtorDefault<int>>::value);
static_assert(std::is_trivially_move_constructible<SCtorDefault<int>>::value);
static_assert(std::is_trivially_copy_assignable<SCtorDefault<int>>::value);
static_assert(std::is_trivially_move_assignable<SCtorDefault<int>>::value);

static_assert(std::is_trivially_copyable<W<S<int>>>::value);
static_assert(std::is_trivially_copy_constructible<W<S<int>>>::value);
static_assert(std::is_trivially_move_constructible<W<S<int>>>::value);
static_assert(std::is_trivially_copy_assignable<W<S<int>>>::value);
static_assert(std::is_trivially_move_assignable<W<S<int>>>::value);

static_assert(std::is_trivially_copyable<W<SCtor<int>>>::value);
static_assert(std::is_trivially_copy_constructible<W<SCtor<int>>>::value);
static_assert(std::is_trivially_move_constructible<W<SCtor<int>>>::value);
static_assert(std::is_trivially_copy_assignable<W<SCtor<int>>>::value);
static_assert(std::is_trivially_move_assignable<W<SCtor<int>>>::value);

static_assert(std::is_trivially_copyable<W<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_copy_constructible<W<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_move_constructible<W<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_copy_assignable<W<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_move_assignable<W<SCtorMutable<int>>>::value);

static_assert(std::is_trivially_copyable<W<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_copy_constructible<W<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_move_constructible<W<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_copy_assignable<W<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_move_assignable<W<SCtorDefault<int>>>::value);

static_assert(std::is_trivially_copyable<WMutable<S<int>>>::value);
static_assert(std::is_trivially_copy_constructible<WMutable<S<int>>>::value);
static_assert(std::is_trivially_move_constructible<WMutable<S<int>>>::value);
static_assert(std::is_trivially_copy_assignable<WMutable<S<int>>>::value);
static_assert(std::is_trivially_move_assignable<WMutable<S<int>>>::value);

static_assert(std::is_trivially_copyable<WMutable<SCtor<int>>>::value); // error with clang
static_assert(std::is_trivially_copy_constructible<WMutable<SCtor<int>>>::value); // error with clang/gcc
static_assert(std::is_trivially_move_constructible<WMutable<SCtor<int>>>::value);
static_assert(std::is_trivially_copy_assignable<WMutable<SCtor<int>>>::value);
static_assert(std::is_trivially_move_assignable<WMutable<SCtor<int>>>::value);

static_assert(std::is_trivially_copyable<WMutable<SCtorMutable<int>>>::value); // error with clang
static_assert(std::is_trivially_copy_constructible<WMutable<SCtorMutable<int>>>::value); // error with clang/gcc
static_assert(std::is_trivially_move_constructible<WMutable<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_copy_assignable<WMutable<SCtorMutable<int>>>::value);
static_assert(std::is_trivially_move_assignable<WMutable<SCtorMutable<int>>>::value);

static_assert(std::is_trivially_copyable<WMutable<SCtorDefault<int>>>::value); // error with clang
static_assert(std::is_trivially_copy_constructible<WMutable<SCtorDefault<int>>>::value); // error with clang/gcc
static_assert(std::is_trivially_move_constructible<WMutable<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_copy_assignable<WMutable<SCtorDefault<int>>>::value);
static_assert(std::is_trivially_move_assignable<WMutable<SCtorDefault<int>>>::value);
Run Code Online (Sandbox Code Playgroud)

Jan*_*tke 4

Clang 是三个编译器中唯一正确的。简而言之,添加mutable到数据成员会导致非平凡的可变参数构造函数在重载决策中胜过平凡的、隐式定义的复制构造函数。这发生在 的复制构造函数中WMutable,因此WMutable不可简单复制。

长答案

一般做的事情mutable是:

  • const 对象是 const 类型的对象const T或 const 对象的不可变子对象。
  • [...]

- https://eel.is/c++draft/basic.type.qualifier#1

这意味着我们的SCtor<int>数据成员不是const,这会影响重载解析。让我们考虑一下类型const WMutable<SCtor<int>>>扩展为什么:

struct const WMutable<SCtor<int>> {
    SCtor<int> m_t;

    // implicitly declared and defined, not actually defaulted
    const_WMutable_SCtor_int(const const_WMutable_SCtor_int&) = default;
    // ...
};
Run Code Online (Sandbox Code Playgroud)

隐式定义或显式默认的复制构造函数复制每个成员。复制成员不一定需要使用复制构造函数:

[...] 否则,基类或成员将使用 的相应基类或成员直接初始化x

- https://eel.is/c++draft/class.copy.ctor#14

这意味着我们得到了以下内容:

// if this was defined by the compiler, it would look like ...
const_WMutable_SCtor_int(const const_WMutable_SCtor_int& other)
  : m_t(other.m_t) {}
Run Code Online (Sandbox Code Playgroud)

m_t将被初始化为 (lvalue) 类型的参数SCtor<int>,并且可以调用两个构造函数:

// (1) this constructor is implicitly declared and defined for SCtor<int>
SCtor(const SCtor&)

// (2) this constructor is user-defined
template<typename... U>
SCtor(U&&... u): m_t(std::forward<U>(u)...) {}
Run Code Online (Sandbox Code Playgroud)

SCtor<int>构造函数 (2) 在重载决策中获胜,因为从 (lvalue)到&的转换序列SCtor<int>比 to 短const SCtoer<int>&

因此,该类型WMutable<SCtor<int>>(以及WMutable示例中的其他专业化)不可轻易复制,因为它违反了要求:

[...] 其中每个合格的复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符都是微不足道的,并且

- https://eel.is/c++draft/class.prop#1

的复制构造函数WMutable<SCtoer<int>>不是平凡的,因此该类不是平凡可复制的,也不是平凡可复制构造的。

GCC 和 MSVC 错误

GCC 和 MSVC 必须错误地将重载集限制为仅复制构造函数,而不是可用于复制成员的其他构造函数。重现此错误的最短方法是:

#include <type_traits>

struct test {
    int member;
    template <typename T>
    test(T&); // not a copy constructor
};

// every compiler agrees and complies, this should pass
static_assert(std::is_trivially_copy_constructible_v<test>);
static_assert(std::is_trivially_copyable_v<test>);

struct wrapper {
    mutable test member;
};

// both should fail, but MSVC allows both due to not considering
// test<T>(T&) as part of the overload set, only its copy constructors
static_assert(std::is_trivially_copy_constructible_v<wrapper>);
static_assert(std::is_trivially_copyable_v<wrapper>);
Run Code Online (Sandbox Code Playgroud)

请参阅编译器资源管理器上的实时示例

然而,对于这个更简单的例子,GCC 和 Clang 都同意。只有 MSVC 不合规(未更改/permissive-)。