Search code examples
c++language-lawyercopy-constructormutabletrivially-copyable

mutable data member, template constructor and trivially copy constructible


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

Solution

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