When is a private constructor not a private constructor?

Angew's and jaggedSpire's' answers are excellent and apply to c++11. And c++14. And c++17.

However, in c++20, things change a bit and the example in the OP will no longer compile:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

As pointed out by the two answers, the reason the latter two declarations work is because C is an aggregate and this is aggregate-initialization. However, as a result of P1008 (using a motivating example not too dissimilar from the OP), the definition of aggregate changes in C++20 to, from [dcl.init.aggr]/1:

An aggregate is an array or a class ([class]) with

  • no user-declared or inherited constructors ([class.ctor]),
  • no private or protected direct non-static data members ([class.access]),
  • no virtual functions ([class.virtual]), and
  • no virtual, private, or protected base classes ([class.mi]).

Emphasis mine. Now the requirement is no user-declared constructors, whereas it used to be (as both users cite in their answers and can be viewed historically for C++11, C++14, and C++17) no user-provided constructors. The default constructor for C is user-declared, but not user-provided, and hence ceases to be an aggregate in C++20.


Here is another illustrative example of aggregate changes:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

B was not an aggregate in C++11 or C++14 because it has a base class. As a result, B{} just invokes the default constructor (user-declared but not user-provided), which has access to A's protected default constructor.

In C++17, as a result of P0017, aggregates were extended to allow for base classes. B is an aggregate in C++17, which means that B{} is aggregate-initialization that has to initialize all the subobjects - including the A subobject. But because A's default constructor is protected, we don't have access to it, so this initialization is ill-formed.

In C++20, because of B's user-declared constructor, it again ceases to be an aggregate, so B{} reverts to invoking the default constructor and this is again well-formed initialization.


You're not calling the default constructor, you're using aggregate initialization on an aggregate type. Aggregate types are allowed to have a defaulted constructor, so long as it's defaulted where it's first declared:

From [dcl.init.aggr]/1:

An aggregate is an array or a class (Clause [class]) with

  • no user-provided constructors ([class.ctor]) (including those inherited ([namespace.udecl]) from a base class),
  • no private or protected non-static data members (Clause [class.access]),
  • no virtual functions ([class.virtual]), and
  • no virtual, private, or protected base classes ([class.mi]).

and from [dcl.fct.def.default]/5

Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor] [class.dtor], [class.copy]), which might mean defining them as deleted. A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted; if such a function is implicitly defined as deleted, the program is ill-formed. [ Note: Declaring a function as defaulted after its first declaration can provide efficient execution and concise definition while enabling a stable binary interface to an evolving code base. — end note ]

Thus, our requirements for an aggregate are:

  • no non-public members
  • no virtual functions
  • no virtual or non-public base classes
  • no user-provided constructors inherited or otherwise, which allows only constructors which are:
    • implicitly declared, or
    • explicitly declared and defined as defaulted at the same time.

C fulfills all of these requirements.

Naturally, you may be rid of this false default construction behavior by simply providing an empty default constructor, or by defining the constructor as default after declaring it:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;

The trick is in C++14 8.4.2/5 [dcl.fct.def.default]:

... A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. ...

Which means that C's default constructor is actually not user-provided, because it was explicitly defaulted on its first declaration. As such, C has no user-provided constructors and is therefore an aggregate per 8.5.1/1 [dcl.init.aggr]:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).