Search code examples
c++constructorlanguage-lawyertype-traits

std::is_constructible for aggregates with invalid default initializer


What std::is_constructible shall return for an aggregate type that does not allow creation of objects due to invalid initializer of a member field?

Consider for example

#include <type_traits>

template <class T>
struct A {
    T x{};
};

static_assert( !std::is_constructible_v<A<int&>> );

A<int&> obj; is not well-formed since it cannot initialize int& from {}. So I would expect that the example program above compiles fine, as it does in GCC. But MSVC accepts the opposite statement static_assert( std::is_constructible_v<A<int&>> ); presumable since the default constructor of A was not formally deleted. And Clang behaves in the third way stopping the compilation with the error:

error: non-const lvalue reference to type 'int' cannot bind to an initializer list temporary
    T x{};
       ^~
: note: in instantiation of default member initializer 'A<int &>::x' requested here
struct A {
       ^

Online demo: https://gcc.godbolt.org/z/nnxcGn7WG

Which one of the behaviors is correct according to the standard?


Solution

  • std::is_constructible_v<A<int&>> checks whether a variable initialization with () initializer is possible. That's never aggregate initialization (not even in C++20), but always value initialization, which will use the default constructor.

    Because your class doesn't declare any constructor, it still has an implicit default constructor declared. That one will be called during value initialization.

    The implicit default constructor is also not defined as deleted, because none of the points in [class.default.ctor]/2 apply. There is one point for reference members without any default member initializer though.

    So this constructor will be chosen in overload resolution and therefore std::is_constructible_v<A<int&>> is true. That the instantiation of the default constructor might be ill-formed is irrelevant. Only declarations are checked (the immediate context). So GCC's behavior is not correct.

    The only remaining question now is whether the implicit default constructor (or rather the default member initializer?) is supposed to be instantiated from the std::is_constructible test itself, causing the program to be ill-formed.

    As far as I can tell the standard doesn't specify that clearly. The standard says that the noexcept-specifier of a function is implicitly instantiated when it is needed. (see [temp.inst]/15)

    It also says that it is needed if it is used in an unevaluated operand in a way that would be an ODR use if it was potentially-evaluated. (see [except.spec]/13.2

    Arguably the type trait has to make such a use.

    Then [except.spec]/7.3 specifies that it needs to be checked whether the default member initializers are potentially-throwing in order to check whether the implicit default constructor has a potentially-throwing exception specification.

    Clang seems to then follow the idea that this requires instantiation of the default member initializer and therefore causes the compilation error because that instantiation is invalid.

    The problems I see with that is:

    1. I don't see anything in the [temp.inst] about when default member initializers are instantiated or what that would mean exactly.
    2. [temp.inst]/15 speaks of the noexcept-specifier grammar construct and since the default constructor is implicit, that doesn't really work out.