Optionally-uninitialized class: std::is_trivially_constructible seems incorrect for non-default constructor?

Viewed 135

I feel like std::is_trivially_constructible<T, Arg> isn't telling me the truth. First, context:

I have a small-vector class, VectorND<T, N>. Default-constructing it default-constructs its members, so VectorND<float, 2>{} == VectorND<float, 2>{float{}, float{}} == VectorND<float, 2>{0.f, 0.f}. This is the desired behavior.

However, sometimes, for perf-critical code, I want to construct them uninitialized. My thought was that I could use a tag type like so:

struct uninitalized_t {};
static constexpr uninitalized_t uninitalized;
...
    VectorND(uninitalized_t) {}
...
VectorND<float, 2> x{uninitalized}; //< Tell x to be uninitialized.

I can get that to work. First, if I do

template <typename T, std::size_t N>
class VectorND {
    std::array<T, 2> x;
public:
    VectorND() = default;
    T& operator[](std::size_t i) { return x[i]; }
};

https://godbolt.org/z/oTvo33xEb

then the default constructor leaves the data uninitialized. Same with if it's VectorND() {}.

If I make it VectorND() : x{} {} then the data is zeroed, making it no longer trivially constructible, as expected. This is the desired default behavior.

But if I add explicit VectorND(uninitalized_t) {}, then VectorND<float, 2> x{uninitialized} does appear to be uninitialized: https://godbolt.org/z/j9KWhPT7n This is again what I want. But for some reason, std::is_trivially_constructible_v<VectorND<float, 2>, uninitalized_t> is false. Why? I've tried

VectorND() = default; // Or VectorND() {};
explicit VectorND(uninitalized_t) {}

and

VectorND() = default; // Or VectorND() {};
explicit VectorND(uninitalized_t) : VectorND() {}

I still get std::is_trivially_constructible_v<VectorND<float, 2>, uninitalized_t> == false. https://godbolt.org/z/3n1Mz9dWd

Why?

1 Answers

Short Answer

This doesn't work because this isn't how trivial constructors are defined by the standard.

Trivial has nothing to do with initialization (or lack-thereof), it's just a set of requirements that the standard lays out to define a constructor as being trivial -- a state which enables better code gen by the compiler and optimizations by library authors.

Long Answer

The state of being trivial, as far as the C++ standard is concerned, has nothing to do with whether members are initialized. A byproduct of being trivial is that you may get uninitialized data from an object undergoing trivial value initialization -- but that does not mean that uninitialized data is the definition of triviality.

Formally speaking, the standard just outlines criterias for very specific constructors to be considered trivial:

  • Default constructors are defined by class.default.ctor/3

    A default constructor is trivial if it is not user-provided and if:

    • its class has no virtual functions ([class.virtual]) and no virtual base classes ([class.mi]), and
    • no non-static data member of its class has a default member initializer ([class.mem]), and
    • all the direct base classes of its class have trivial default constructors, and
    • for all the non-static data members of its class that are of class type (or array thereof), each such class has a trivial default constructor.

    Otherwise, the default constructor is non-trivial.

  • Copy/Move constructors are defined by class.copy.ctor/11

    A copy/move constructor for class X is trivial if it is not user-provided and if:

    • class X has no virtual functions ([class.virtual]) and no virtual base classes ([class.mi]), and
    • the constructor selected to copy/move each direct base class subobject is trivial, and
    • 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;

    otherwise the copy/move constructor is non-trivial.

Note: There are also similar requirements for operator= and for the destructor

There are not, however, any actual definitions for a custom constructor that is not a copy, move, or default constructor to be trivial. This means that any custom constructor cannot be considered trivial.

This also effectively means that std::is_trivially_constructible<T,Args...>::value can only evaluate to true if it tests for the default constructor, copy constructor, or move constructor.

Why does being Trivial matter?

The state of being "trivial" exists because it gives better optimization guarantees both to the compiler and to library authors. Trivial constructors for copy/moving amount to simple mov instructions to just copy around the data (as opposed to requiring any cleanup or rewiring that an object's constructor may usually require).

Additionally, if a type satisfies enough trivial requirements, it may be "Trivially Copyable" -- which allows compilers and library authors to simply copy the data around with std::memcpy rather than requiring constructor calls. This can also be used to view a different object representation without violating strict aliasing.

Related