Trivially copyable does not mean trivially copy constructible

About a month ago, I got an interesting pull request for lexy, my new parser combinator library. It fixed a seemingly weird issue relating trivially copyable types and special member function of classes containing unions. While digging into it, I learned a lot about trivial special member functions and made a somewhat surprising realization:

Just because a class is std::is_trivially_copyable does not mean the class is actually std::is_trivially_copy_constructible or even std::is_copy_constructible: you can have classes that you can’t copy, but they’re still trivially copyable, and classes where the copy constructor can do arbitrary amounts of non-trivial work, but they’re nonetheless trivially copyable!

Let me explain.

Special member function

The default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator, and destructor of a class are called special member function. They’re special, because the compiler can and will implement them for us in certain situations. The exact rules are complicated, but luckily we don’t need to bother with them here (nor ever).

A default constructor of a class T is a constructor that can be called without arguments:

T(); // ok, no arguments
T(int i = 42, float f = 3.14); // ok, all arguments defaulted
template <typename ... Args>
T(const Args&... args); // ok, can be called with no arguments

A copy constructor of a class T is a non-templated (!) constructor whose first argument is of type T&, const T&, volatile T&, or const volatile T&, and all other parameters (if there are any) have default arguments. Likewise, a move constructor of a class T is a non-templated (!) constructor whose first argument is of type T&&, const T&&, volatile T&& or const volatile T&&, and all other parameters (if there are any) have default arguments.

T(const T& other); // traditional copy constructor
T(T&& other); // traditional move constructor

T(const T& other, int i = 42); // copy constructor, second argument defaulted

T(T& other); // copy constructor

template <typename Arg>
T(Arg&& other); // not a copy/move constructor, templated

A copy assignment operator of a class T is a non-templated (!) operator= overload whose only argument is of type T&, const T&, volatile T&, or const volatile T&. Likewise, a move assignment operator of a class T is a non-templated (!) operator= overload whose only argument is of type T&&, const T&&, volatile T&&, or const volatile T&&. Note that the return type or member function cv/ref qualifier don’t matter.

T& operator=(const T& other); // traditional copy assignment
T& operator=(T&& other); // traditional move assignment

int operator=(const T& other) volatile &&; // copy assignment

template <typename Arg>
T& operator=(Arg&& other); // not a copy/move assignment, templated

A destructor is the weird member function with the ~.

Keep those rules in mind, they’ll become important later.

Type traits for special member functions

Each special member function has a type trait std::is_[default/copy/move]_[constructible/assignable] that allows you to query its existence. So if a class has a copy constructor, std::is_copy_constructible<T> is std::true_type.

Except this is not what those traits actually do!

The traits query whether an expression is well-formed:

This means that the type traits can report a different results from a hypothetical “does the class have this special member function?” trait. For starters, they ignore access specifiers: if you have a private copy constructor, std::is_copy_constructible<T> is std::false_type. But there are also more nuances in some situations:

struct weird
{
    weird& operator=(const volatile weird&) = delete; // (1)

    template <int Dummy = 0>
    weird& operator=(const weird&) // (2)
    {
        return *this;
    }
};

static_assert(std::is_copy_assignable_v<weird>); // ok

weird w;
w = w; // invokes (2)

godbolt link

Note that overload (2) takes a defaulted template parameter. Using operator syntax to call that function, there is no way to actually specify that template parameter. However, as it is defaulted, there is no need to.

The operator overload (1) is a copy assignment operator, which is deleted. The operator overload (2) is not considered to be an assignment operator, as it is a template. However, overload resolution of w = w doesn’t care about what exactly is a “copy assignment operator”, it just works as normal. As such, it will find the templated overload (which is a better match than the one taking a const volatile), and happily “copy assign” the object, even though it technically has no copy assignment operator. This is also what std::is_copy_assignable checks, so the assertion passes.

I’m focusing on assignment here and the following for dramatic reasons, but the same applies to every other special member function.

The rules that determine whether something is a special member function and the rules that determine which constructor/assignment operator is actually invoked are completely different!

To determine whether something is special member function, look for a member with the signatures given above. To determine what is called, do regular overload resolution.

This is especially problematic when combining it with the rules for implicit generation of special member functions. Consider a class that defines only a templated assignment operator, but not one that is considered to be a copy assignment operator. The compiler might generate a copy assignment operator in addition to the templated one, which is then preferred during overload resolution! (Unlike weird, the generated copy assignment operator takes const weird&, which is then preferred).

Note that the type traits, which do overload resolution, give you the correct result. Something like std::has_copy_assignment_operator<T> wouldn’t be very useful, as you want to query whether you can invoke something that looks like one, not whether there is the corresponding function somewhere.

See also: Arthur O’Dwyer’s post about a concept that checks whether there exists a member function with a given signature.

Trivial special member function

Special member functions can be trivial (not the topic, the actual member function can have this property). They are trivial, if they’re not user provided (i.e. they use = default or are implicitly generated), and the corresponding function of all the members/base classes are also trivial. Trivial default constructors and destructors do nothing, whereas trivial copy/move constructors/assignment operator do essentially a std::memcpy.

struct foo
{
    int a;
    float f;

    foo() = default; // trivial

    // implicitly declared copy constructor is trivial

    ~foo() {} // not-trivial, user provided
};

The actual rules are bit more nuanced, but you’ll get the idea.

Type traits for trivial special member functions

Each of the six type traits from above also come in a is_trivially_XXX flavor. And again, they don’t check whether the type has a trivial special member function, but whether the corresponding expression invokes only trivial functions.

struct weird
{
    weird& operator=(const volatile weird&) = delete; // (1)

    template <int Dummy = 0>
    weird& operator=(const weird&) // (2)
    {
        return *this;
    }
};

static_assert(std::is_copy_assignable_v<weird>); // ok
// not ok, (2) is non-trivial
static_assert(std::is_trivially_copy_assignable_v<weird>);

godbolt link

Again, this is what is useful: you want to check whether a = b invokes a non-trivial function, not whether there is a non-trivial function in the class.

std::is_trivially_copyable

This brings me to std::is_trivially_copyable, which does something completely different from std::is_trivially_copy_constructible!

std::is_trivially_copyable<T> checks whether T is a trivially copyable type (duh). A trivially copyable type is either a fundamental type, or a class where:

  1. the destructor is trivial and not deleted,
  2. every copy/move constructor/assignment operator is either deleted or trivial (or doesn’t exist at all),
  3. and there is a non-deleted copy constructor, move constructor, copy assignment operator, or move assignment operator.

Condition 1 should be straightforward: the destructor of the type must not do anything. Condition 2 says that if the type has a special member function, it must be trivial. Finally, condition 3 says that there must be some way to relocate an object from one location to another; types that are completely immovable are not trivially copyable.

Again, the actual rules are bit more nuanced.

Note that std::is_trivially_copyable_v<T> can be true, but std::is_trivially_copy_constructible_v<T> can be false: T does not need to be copy constructible to be trivially copyable, std::is_copy_constructible_v<T> can be false.

There is also std::is_trivial which is std::is_trivially_copyable plus trivial default constructor. However, this type trait is neither interesting nor really useful compared to trivially copyability, so I won’t be mentioning it again.

Got all that? Because now it gets interesting.

Based on the definition above, you might be tempted to implement std::is_trivially_copyable_v<T> as follows:

template <typename T>
constexpr bool is_trivially_copyable_v
  // condition 1
  = std::is_trivially_destructible_v<T>
  // condition 2
  && (!std::is_copy_constructible_v<T> || std::is_trivially_copy_constructible_v<T>)
  && (!std::is_move_constructible_v<T> || std::is_trivially_move_constructible_v<T>)
  && (!std::is_copy_assignable_v<T> || std::is_trivially_copy_assignable_v<T>)
  && (!std::is_move_assignable_v<T> || std::is_trivially_move_assignable_v<T>)
  // condition 3
  && (std::is_copy_constructible_v<T> || std::is_move_constructible_v<T>
    || std::is_copy_assignable_v<T> || std::is_move_assignable_v<T>);

In fact, this is basically how clang implements std::is_trivially_copyable currently.

But this implementation is wrong!

Unlike std::is_trivially_[copy/move]_[constructible/assignable], std::is_trivially_copyable does not use overload resolution to check expressions. It actually goes ahead and looks for the existence of a special member function!

This can create funny situations:

struct weird
{
    weird() = default;
    weird(const weird&) = default;
    weird(weird&&)      = default;
    ~weird() = default;

    weird& operator=(const volatile weird&) = delete; // (1)

    template <int Dummy = 0>
    weird& operator=(const weird&) // (2)
    {
        return *this;
    }
};

static_assert(std::is_copy_assignable_v<weird>); // (a)
static_assert(!std::is_trivially_copy_assignable_v<weird>); // (b)
static_assert(std::is_trivially_copyable_v<weird>); // (c)

godbolt link

Assertion (a) passes because overload resolution finds the templated overload (2). Assertion (b) does not pass because overload resolution checks the templated overload (2), which is not trivial.

However, assertion (c) passes (if you don’t use clang, that is): std::is_trivially_copyable_v<weird> checks the special member functions without doing overload resolution. It has a trivial non-deleted destructor and copy/move constructor, and a deleted copy assignment operator. As such, it is trivially copyable.

That the actual copy assignment a = b might invoke arbitrary non-trivial code does not matter, the type is still trivially copyable!

Just because a type is copy assignable and trivially copyable, does not mean that the type is trivially copy assignable, likewise for all the other special member functions.

Okay, that’s a bit weird. But surely nobody writes types such as weird and the important type trait is either std::is_trivially_copyable or one of the std::is_trivially_[copy/move]_[constructible/assignable] and not a mix between the two depending on the situation.

… you know what is coming?

weird is known as Microsoft’s std::pair and the standard absolutely requires sometimes std::is_trivially_copyable and sometimes std::is_trivially_[copy/move]_[constructible/assignable] depending on the situation!

Trivially copyability vs. calls trivial function

The standard requires that a type is std::is_trivially_copyable in the following situations:

On the other hand, the standard requires that overload resolution invokes only trivial special member functions (std::is_trivially_[copy/move]_[constructible/assignable])

The union cases are interesting: Copying a union is defined to copy the object representation, which essentially does std::memcpy. std::memcpy is only allowed for trivially copyable types. However, the union only has a non-deleted copy constructor if overload resolution finds a trivial copy constructor for all variants, which is not guaranteed to exist for trivially copyable types!

This means that it is not enough to put std::is_trivially_copyable types into a union, they need to actually be std::is_trivially_[copy/move]_[constructible/assignable] – even though the actual copy operation requires only std::is_trivially_copyable:

// As above.
struct weird
{
    weird() = default;
    weird(const weird&) = default;
    weird(weird&&)      = default;
    ~weird() = default;

    weird& operator=(const volatile weird&) = delete;

    template <int Dummy = 0>
    weird& operator=(const weird&)
    {
        return *this;
    }
};

static_assert(std::is_copy_assignable_v<weird>);
static_assert(!std::is_trivially_copy_assignable_v<weird>);
static_assert(std::is_trivially_copyable_v<weird>);

union weird_union
{
    int i;
    weird w;
} u;
u = u; // error: weird_union has deleted copy assignment

godbolt link

And remember: weird is more commonly known as std::pair. This was exactly the cause of lexy’s initial bug.

I’ll just leave you with the tweet I wrote after I figured it all out:

(the standard’s behavior is a bit surprising, MSVC’s std::pair is not trivially copy assignable, and clang doesn’t do std::is_trivially_copyable correctly)

Conclusion

There are two different categories of type traits regarding trivial special member function: std::is_trivially_[copy/move]_[constructible/assignable] and std::is_trivially_copyable. The first category do overload resolution to evaluate some expression and determine whether the called function is trivial, the second category look whether the class defines functions matching a given signature.

This makes them fundamentally incompatible.

The type traits you actually want most of the time are in the first category: you actually type some expression in your code and want to check whether that is trivial. Use them to constrain your special member functions or select between a trivial and non-trivial union based implementation.

std::is_trivially_copyable should only be used when you need to call std::memcpy() or std::bit_cast() (or functions that are built on top). In particular, do not use them as a shorthand for “all special member functions are trivial”, because that is not what it actually does!

Always remember: a type can be std::is_trivially_copyable without being std::is_trivially_copy_constructible or std::is_copy_constructible: types with deleted copy constructor can be trivially copyable, and types where overload resolution selects a non-trivial constructor during copy can still have a trivial copy constructor.

If you've liked this blog post, consider donating or otherwise supporting me.