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 anexplicit
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 anexplicit
constructor) - C++20: None of
Foo
orBar
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.