Purpose of Explicit Default Constructors

Unless explicitly stated otherwise, all standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.


(This answer focus specifically on explicit default constructors which have no parameters)


Case #1 [C++11 through C++20]: Empty {} copy-list-initialization for non-aggregates prohibits use of explicit default constructors

As governed by [over.match.list]/1 [emphasis mine]:

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 section, overload resolution selects the constructor in two phases:

  • (1.1) Initially, 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.
  • (1.2) If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

If the initializer list has no elements and T has a default constructor, the first phase is omitted. In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed. [ Note: This differs from other situations ([over.match.ctor], [over.match.copy]), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution.  — end note ]

copy-list-initialization with an empty braced-init-list {} for non-aggregates prohibits use of explicit default constructors; e.g.:

struct Foo {
    virtual void notAnAggregate() const {};
    explicit Foo() {}
};

void foo(Foo) {}

int main() {
    Foo f1{};    // OK: direct-list-initialization

    // Error: converting to 'Foo' from initializer
    // list would use explicit constructor 'Foo::Foo()'
    Foo f2 = {};
    foo({});
}

Albeit the standard quote above refers to C++17, this likewise applies for C++11, C++14 and C++20.


Case #2 [C++17 only]: A class type with a user-declared constructor that is marked as explicit is not an aggregate

[dcl.init.aggr]/1 added was updated some between C++14 and C++17, mainly by allowing an aggregate to derive publicly from a base class, with some restrictions, but also prohibiting explicit constructors for aggregates [emphasis mine]:

An aggregate is an array or a class with

  • (1.1) no user-provided, explicit, or inherited constructors ([class.ctor]),
  • (1.2) no private or protected non-static data members (Clause [class.access]),
  • (1.3) no virtual functions, and
  • (1.4) no virtual, private, or protected base classes ([class.mi]).

As of P1008R1 (Prohibit aggregates with user-declared constructors), which has been implemented for C++20, we may no longer ever declare constructors for aggregates. In C++17 alone, however, we had the peculiar rule that whether a user-declared (but not user-provided) constructor was marked explicit decided whether the class type was an aggregate or not. E.g. the class types

struct Foo {
    Foo() = default;
};

struct Bar {
    explicit Bar() = default;
};

were aggregates/not aggregates in C++11 through C++20 as follows:

  • C++11: Foo & Bar are both aggregates
  • C++14: Foo & Bar are both aggregates
  • C++17: Only Foo is an aggregate (Bar has an explicit constructor)
  • C++20: None of Foo or Bar are aggregates (both has user-declared constructors)

This declares an explicit default constructor:

struct A {
  explicit A(int a1 = 0);
};

A a = 0; /* not allowed */
A b; /* allowed */
A c(0); /* allowed */

In case there is no parameter, like in the following example, the explicit is redundant.

struct A {
  /* explicit is redundant. */
  explicit A();
};

In some C++0x draft (I believe it was n3035), it made a difference in the following way:

A a = {}; /* error! */
A b{}; /* alright */

void function(A a);
void f() { function({}); /* error! */ }

But in the FCD, they changed this (though, I suspect that they didn't have this particular reason in mind) in that all three cases value-initialize the respective object. Value-initialization doesn't do the overload-resolution dance and thus won't fail on explicit constructors.