c++constructorlanguage-lawyerinitializer-list

How are we able to default construct object without a default constructor


I learned that when a class has a user-provided constructor then the compiler will not implicitly generate a default constructor. I wrote the following code, C d{}; in particular, and I expect it to fail (give an error) because, as per my understanding, there is no default constructor for this class.

But, to my surprise, the program compiles with all compilers (gcc, clang, msvc, edg).

Demo

#include <initializer_list>

struct C
{
    
    //we've a user provided function so no default ctor will be generated by compiler afaik
    C(std::initializer_list<int> i)
    {

    }
    void func(){}
    
};

int main()
{
    C d{};   //GCC: ok, Clang:Ok, MSVC: Ok, EDG:Ok 
    d.func();
}

So my question is, why/how does the program, C d{}; in particular, work when there is no default constructor for C?


Solution

  • The program is well-formed as explained below.

    First note that C d{}; is list initialization. So we move onto dcl.init#list-3:

    1. List-initialization of an object or reference of type cv T is defined as follows:
    • 3.1 [...]
    • 3.2 [...]
    • 3.3 [...]
    • 3.4 [...]
    • 3.5 [...]
    • 3.6 [...]
    • 3.7 Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

    This means that all constructors including the initializer list ctor C::C(std::initializer_list<int>) will be considered.

    So we move onto  over.match.list :

    When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:

    • If the initializer list is not empty or T has no default constructor, overload resolution is first performed where the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.

    This means that first the initializer list ctor will be considered with the initializer list {} as argument. And since a parameter of type std::initializer_list can be initialized with {}, the initializer list ctor will be used.