What is the purpose of "explicit" for a default constructor?
Asked Answered
C

2

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

Cheek answered 14/5, 2010 at 19:16 Comment(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.Cheek
D
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.

Dilate answered 14/5, 2010 at 19:24 Comment(9)
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.Cheek
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.Animal
@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 :/Dilate
@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.Dilate
@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."Dilate
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: 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.Dilate
We had a similar issue back in the days when preparing C++11: 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).Dilate
Recent versions of g++ and clang also reject A a = {};, and struct C { A a; }; with a subsequent C c{};Luwian
@m.m yes they didn't want to change this and decided they want to require nonexplicit ctors.Dilate
R
8

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)
Reduction answered 23/9, 2020 at 18:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.