55

I recently noticed a class in C++0x that calls for an explicit default constructor. However, I'm failing to come up with a scenario in which a default constructor can be called implicitly. It seems like a rather pointless specifier. I thought maybe it would disallow Class c; in favor of Class c = Class(); but that does not appear to be the case.

Some relevant quotes from the C++11 FCD, since it is easier for me to navigate [similar text exists in C++03, if not in the same places]

12.3.1.3 [class.conv.ctor]

A default constructor may be an explicit constructor; such a constructor will be used to perform default-initialization or value initialization (8.5).

It goes on to provide an example of an explicit default constructor, but it simply mimics the example I provided above.

8.5.6 [decl.init]

To default-initialize an object of type T means:

— if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);

8.5.7 [decl.init]

To value-initialize an object of type T means:

— if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);

In both cases, the standard calls for the default constructor to be called. But that is what would happen if the default constructor were non-explicit. For completeness sake:

8.5.11 [decl.init]

If no initializer is specified for an object, the object is default-initialized;

From what I can tell, this just leaves conversion from no data. Which doesn't make sense. The best I can come up with would be the following:

void function(Class c); int main() { function(); //implicitly convert from no parameter to a single parameter } 

But obviously that isn't the way C++ handles default arguments. What else is there that would make explicit Class(); behave differently from Class();?

The specific example that generated this question was std::function [20.8.14.2 func.wrap.func]. It requires several converting constructors, none of which are marked explicit, but the default constructor is.

1
  • As soon as I hit post, I think I came up with an explanation. But I'll wait for confirmation of my suspicions, since this seems like a useful question anyhow. Commented May 14, 2010 at 19:21

2 Answers 2

46

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.

Sign up to request clarification or add additional context in comments.

9 Comments

Okay. Then the explicit constructor for std::function is simply a hold over from that version of the draft? It was this explanation that I finally figured out after writing the question, but neither the provided example nor std::function() took an optional parameter, so I wasn't entirely convinced.
This seems to have changed a bit, see CWG 1518. Recent versions of g++ and clang++ reject function({}) for non-defaulted explicit default constructors, even in C++11 mode.
@VilleVoutilainen that seems to be another wart of the core language. Why do we not consider explicit default constructors for default initialization triggered by = {} when value initializing, but do consider explicit constructors when looking for constructors triggered by = { foo } when not value initializing? My first reaction to that LWG issue resolution was "oh no that will not work, because list-initialization considers explicit constructors, and only when they are chosen the program is ill-formed.". Wrong litb, C++ has another warty special case here for extra confusion :/
@VilleVoutilainen in coliru.stacked-crooked.com/a/25673a34a62d668e , GCC says that the call is ambiguous. Yet if you remove the void f(B) overload ( coliru.stacked-crooked.com/a/7ee73c36f3d346e0 ), it complains about the use of explicit constructors. It looks like GCC needs some polishing aswell here (GCC6.3) to implement this IMO wart aswell.
@VilleVoutilainen actually, I think my initial reaction seems to be correct and GCC's behavior is correct aswell, but the LWG resolution is incorrect. Because the language specifies that during overload resolution, the {} -> A conversion sequence has no special case for default initialization, but uses the general "consider explicit ctors, and reject if an explicit one is chosen": Quote: "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."
|
10

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)

2 Comments

In all versions of C++ since C++11, not just C++17, any user-declared (or supplied) explicit constructor prevents a class from being treated as an aggregate, and thus prohibits aggregate initialization.
@leek Are you sure? The wording about explicit was added to [dclt.init.aggr] only in the C++17 standard and, before that, classes with explicit user-declared (but not user-provided) constructors were still aggregates (unless disqualified by other means). The distinction between user-declared and user-provided is essential prior to C++20, and the explicit passage was added and relevant only for C++17. See The fickle aggregate for relevant standard and historic passages.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.