Example code could be found below or on godbolt. Say we have 4 classes:
S<T>
: holding a data member.
SCtor<T>
: holding a data member and has a template constructor.
SCtorMutable<T>
: holding a mutable data member and has a template constructor.
SCtorDefault<T>
: holding a member, has a template constructor, has defaulted copy/move constructors and defaulted copy/move assignment operators.
All compilers agree that these 4 classes are trivially copyable.
If there is a simple wrapper class W<T>
holding any of the above class as a data member. The wrapper class W<S...<T>>
is still trivially copyable.
If there is another wrapper class WMutable<T>
holding any of the above class as a mutable data member.
WMutable<S...<T>>
is trivially copyable.WMutable<S<T>>
is trivially copyable. WMutable<SCtor...<T>>
is not trivially copy constructible therefore not trivially copyable.WMutable<S<T>>
is trivially copyable. WMutable<SCtor...<T>>
is not trivially copy constructible BUT trivially copyable.Should WMutable<T>
be trivially copyable?
#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);
Clang is the only correct of the three compilers. The short answer is that adding mutable
to the data member results in the non-trivial variadic constructor winning in overload resolution over the trivial, implicitly defined copy constructor. This happens in the copy constructor of WMutable
, so WMutable
is not trivially copyable.
What mutable
generally does is:
- A const object is an object of type
const T
or a non-mutable subobject of a const object.- [...]
- https://eel.is/c++draft/basic.type.qualifier#1
This means that our SCtor<int>
data member is not const
, which impacts overload resolution.
Let's consider what the type const WMutable<SCtor<int>>>
expands to:
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;
// ...
};
An implicitly defined or explicitly defaulted copy constructor copies each member. Copying a member may not necessarily use a copy constructor:
[...] otherwise, the base or member is direct-initialized with the corresponding base or member of
x
.
- https://eel.is/c++draft/class.copy.ctor#14
This means that we get something along the lines of:
// 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) {}
m_t
will be initialized to an argument of type (lvalue) SCtor<int>
, and there are two constructors that this can call:
// (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)...) {}
Constructor (2) wins in overload resolution, because the conversion sequence from (lvalue) SCtor<int>
to SCtor<int>
& is shorter than to const SCtoer<int>&
.
As a result, the type WMutable<SCtor<int>>
(and other specializations of WMutable
in your example) is not trivially copyable, because it violates the requirement:
[...] where each eligible copy constructor, move constructor, copy assignment operator, and move assignment operator is trivial, and
- https://eel.is/c++draft/class.prop#1
The copy constructor of WMutable<SCtoer<int>>
is not trivial, and so the the class is not trivially copyable, and not trivially copy-constructible.
GCC and MSVC must falsely restrict the overload set to only copy constructors, not additional constructors that can be used for copying members. The shortest way to reproduce this bug is:
#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>);
See live example on Compiler Explorer
However, for this more simple example, GCC and Clang agree. Only MSVC is non-compliant (unchanged by /permissive-
).