49

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++0x 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.

Community
  • 1
  • 1
Dennis Zickefoose
  • 10,791
  • 3
  • 29
  • 38
  • 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. – Dennis Zickefoose May 14 '10 at 19:21

2 Answers2

42

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.

cbuchart
  • 10,847
  • 9
  • 53
  • 93
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • 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. – Dennis Zickefoose May 14 '10 at 19:35
  • This seems to have changed a bit, see [CWG 1518](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1518). Recent versions of g++ and clang++ reject `function({})` for non-defaulted explicit default constructors, even in C++11 mode. – dyp Apr 13 '15 at 20:02
  • 1
    @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 :/ – Johannes Schaub - litb Feb 07 '17 at 20:38
  • @VilleVoutilainen in http://coliru.stacked-crooked.com/a/25673a34a62d668e , GCC says that the call is ambiguous. Yet if you remove the `void f(B)` overload ( http://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. – Johannes Schaub - litb Feb 07 '17 at 20:57
  • @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."* – Johannes Schaub - litb Feb 07 '17 at 21:05
  • GCC's behavior is just incorrect (all with regard to the draft that I looked at, and assuming I haven't missed something) with regard to this testcase: http://coliru.stacked-crooked.com/a/449ad727f2ca3575 which I think should take the second constructor. It just doesn't work this way when building up a conversion sequence for `{} -> A`. I hope that this will be fixed for C++17, so that overload resolution (in clauses 13.3.3.1.5) correctly will model the semantics of the parameter initialization. – Johannes Schaub - litb Feb 07 '17 at 21:19
  • We had a similar issue back in the days when preparing C++11: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1229 . The issue then was fixed by adding the "If the initializer list has no elements and T has a default constructor, the first phase is omitted. " (therefore ignoring the two initializer list constructors, and then using the default constructors). – Johannes Schaub - litb Feb 07 '17 at 21:26
  • Recent versions of g++ and clang also reject `A a = {};`, and `struct C { A a; };` with a subsequent `C c{};` – M.M Mar 02 '18 at 04:28
  • @m.m yes they didn't want to change this and decided they want to require nonexplicit ctors. – Johannes Schaub - litb Mar 02 '18 at 15:43
6

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)
dfrib
  • 70,367
  • 12
  • 127
  • 192