mutable data member, template constructor and trivially copy constructible

192 views Asked by At

Example code could be found below or on godbolt. Say we have 4 classes:

  1. S<T>: holding a data member.

  2. SCtor<T>: holding a data member and has a template constructor.

  3. SCtorMutable<T>: holding a mutable data member and has a template constructor.

  4. 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.

  1. MSVC still believes WMutable<S...<T>> is trivially copyable.
  2. clang believes WMutable<S<T>> is trivially copyable. WMutable<SCtor...<T>> is not trivially copy constructible therefore not trivially copyable.
  3. gcc believes 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);
2

There are 2 answers

0
Jan Schultke On BEST ANSWER

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.

The Long Answer

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 Bugs

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-).

0
maxplus On
  • Is WMutable<SCtor...<T>> trivially copy-constructible?

No. In general it is not copy-constructible at all. Its implicit copy constructor attempts to copy-construct WMutable::m_t from a non-const reference, because copied-from WMutable::m_t is mutable. Not-user-provided copy-constructor of SCtor...<T> (defaulted for SCtorDefault and implicitly declared for SCtor, SCtorMutable) accepts a const reference, therefore a variadic template universal reference constructor is selected instead. Even if it can construct T from SCtor...<T>, it is user-provided.

A copy/move constructor for class X is trivial if it is not user-provided, its declared parameter type is the same as if it had been implicitly declared, and if
...
— for each non-static data member of X that is of class type (or array thereof), the constructor selected to copy/move that member is trivial;

  • Is WMutable<SCtor...<T>> trivially copyable?

No, it has a non-trivial copy constructor.

A trivially copyable class is a class that:
— has no non-trivial copy constructors (12.8),
...

So, WMutable<S<int>> satisfies all conditions for a trivially copyable class, for WMutable<S<T>> it (obviously) depends, and all WMutable<SCtor...<T>> are not trivially copyable.