Deleted Default Constructor. Objects Can Still Be Created... Sometimes

Deleted default constructor. Objects can still be created... sometimes

When viewing things this way it is easy to say there is complete and utter chaos in the way an object is initialized.

The big difference comes from the type of foo: if it is an aggregate type or not.

It is an aggregate if it has:

  • no user-provided constructors (a deleted or defaulted function does not count as user-provided),
  • no private or protected non-static data members,
  • no brace-or-equal-initializers for non-static data members (since c++11 until (reverted in) c++14)
  • no base classes,
  • no virtual member functions.

So:

  • in scenarios A B D E: foo is an aggregate
  • in scenarios C: foo is not an aggregate
  • scenario F:

    • in c++11 it is not an aggregate.
    • in c++14 it is an aggregate.
    • g++ hasn't implemented this and still treats it as a non-aggregate even in C++14.

      • 4.9 doesn't implement this.
      • 5.2.0 does
      • 5.2.1 ubuntu doesn't (maybe a regression)

The effects of list initialization of an object of type T are:

  • ...
  • If T is an aggregate type, aggregate initialization is performed. This takes care of scenarios A B D E (and F in C++14)
  • Otherwise the constructors of T are considered in two phases:

    • All constructors that take std::initializer_list ...
    • otherwise [...] all constructors of T participate in overload resolution [...] This takes care of C (and F in C++11)
  • ...

:

Aggregate initialization of an object of type T (scenarios A B D E (F c++14)):

  • Each non-static class member, in order appearance in the class definition, is copy-initialized from the corresponding clause of the
    initializer list. (array reference omitted)

TL;DR

All these rules can still seem very complicated and headache inducing. I personally over-simplify this for myself (if I thereby shoot myself in the foot then so be it: I guess I will spend 2 days in the hospital rather than having a couple of dozen days of headaches):

  • for an aggregate each data member is initialized from the elements of the list initializer
  • else call constructor

Doesn't this beat the whole purpose of a deleted constructor?

Well, I don't know about that, but the solution is to make foo not an aggregate. The most general form that adds no overhead and doesn't change the used syntax of the object is to make it inherit from an empty struct:

struct dummy_t {};

struct foo : dummy_t {
foo() = delete;
};

foo f{}; // ERROR call to deleted constructor

In some situations (no non-static members at all, I guess), an alternate would be to delete the destructor (this will make the object not instantiable in any context):

struct foo {
~foo() = delete;
};

foo f{}; // ERROR use of deleted function `foo::~foo()`

This answer uses information gathered from:

  • C++14 value-initialization with deleted constructor

  • What are Aggregates and PODs and how/why are they special?

  • List initialization

  • Aggregate initialization
  • Direct initialization

Many thanks to @M.M who helped correct and improve this post.

Why does 'A a{};' compile when the default constructor A::A() is deleted?

This is a current language issue that is very likely to be fixed soon. The proposal that tackles the necessary design change can be found here. From the abstract of the proposal:

C++ currently allows some types with user-declared constructors to be initialized via aggregate
initialization, bypassing those constructors. The result is code that is surprising, confusing,
and buggy

Could a class type with deleted default constructor be default initialized?

I think the standard just means "if T is a class type with deleted default constructor then goto default initialization". It would fail at last because the constructor selected for default initialization is deleted. It's used to distinguish with the 2nd case, i.e. "if T is a class type with a default constructor that is neither user-provided nor deleted", for that case zero-initialization is performed firstly, then default-initialization if T has a non-trivial default constructor.

A a{} is OK,but why?

Because when A is an aggregate type aggregate initialization is performed. Note that explicitly deleted constructors are allowed for aggregate type since C++11.

In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.

And

An aggregate is one of the following types:

  • ...
  • class type (typically, struct or union), that has
    • ...
    • no user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed) (since C++17) (until C++20)
    • ...

EDIT

Since C++20 the behavior changed; A a{}; would fail.

An aggregate is one of the following types:

  • ...
  • class type (typically, struct or union), that has
    • ...
    • no user-declared or inherited constructors (since C++20)
    • ...

A is not an aggregate again. A a{}; performs value-initialization (default-initialization), the deleted constructor is used and fails.

Is implicitly deleted default constructor same as Compiler not synthesizing the default constructor

This is covered in [class.default.ctor]:

If there is no user-declared constructor for class X, a non-explicit constructor having no parameters is implicitly declared as defaulted ([dcl.fct.def])

So if there are user-declared constructors, there is no creation of an "implicitly declared" defaulted constructor.

The broader question is this: why isn't an implicitly declared default constructor deleted instead of not added at all?

Well, here's the definition of what a default constructor is:

A default constructor for a class X is a constructor of class X for which each parameter that is not a function parameter pack has a default argument (including the case of a constructor with no parameters).

Basically, a constructor is a default constructor if you can call it with no arguments. So it's not defined by a single signature; it's define by calling behavior.

But a default constructor declaration is also a declaration of a user-declared constructor. So if you declare a default constructor, the compiler will not implicitly declare its own. Because otherwise you'd have two default constructors, which is... unhelpful when by definition they can both be called with no arguments (and therefore nothing to overload on) and one of them is deleted.

So yes, it's incorrect to say that it is deleted if you provide a user-declared constructor.

Constructor implicitly deleted

The class member int& ref needs to be bound to an int when an object of type F is created.

A compiler-generated default constructor would not know how to bind it, so the compiler merely deletes the default constructor.

In the second snippet you explicitly set the member. (You can do that from C++11).

A deleted default constructor could still be trivial?

CWG issue 667 addressed this exact issue with a change that was incorporated into the C++ working draft near N3225. N3225 § 12.1 [class.ctor]/5 states:

A default constructor is trivial if it is neither user-provided nor deleted and if:

  • its class has no virtual functions (10.3) and no virtual base classes (10.1), and
  • no non-static data member of its class has a brace-or-equal-initializer, and
  • all the direct base classes of its class have trivial default constructors, and
  • for all the non-static data members of its class that are of class type (or array thereof), each such class has a trivial default constructor.

Otherwise, the default constructor is non-trivial.

This was (obviously) changed before C++11 release. CWG DR 1135 was created to address a Finland national body comment on the C++11 candidate draft:

It should be allowed to explicitly default a non-public special member function on its first declaration. It is very likely that users will want to default protected/private constructors and copy constructors without having to write such defaulting outside the class.

The resolution of this issue removed the "nor deleted" text from 12.1 as well as the sections describing trivial destructors, trivial copy/move constructors, and trivial copy/move assignment operators. I think this change cut too broad a swath, and it was likely not intentional to make your struct A trivial. Indeed, on face value it's ridiculous that this program is ill-formed:

int x = 42;
int y = 13;
A a_x{x};
A a_y{y};
a_x = a_y;

but this program is not, since A is trivially copyable (Clang agrees, GCC does not):

int x = 42;
int y = 13;
A a_x{x};
A a_y{y};
std::memcpy(&a_x, &a_y, sizeof(a_x));

The existence of CWG issue 1496 "Triviality with deleted and missing default constructors" seems to indicate that the committee is aware of the problem (or at least a closely related problem):

A default constructor that is defined as deleted is trivial, according to 12.1 [class.ctor] paragraph 5. This means that, according to 9 [class] paragraph 6, such a class can be trivial. If, however, the class has no default constructor because it has a user-declared constructor, the class is not trivial. Since both cases prevent default construction of the class, it is not clear why there is a difference in triviality between the cases.

although there is no resolution as yet.

C++11 deleted/defaulted constructors

1, Compiles. List init's for A and A1, and the default copy constructor for A2

What you call List init in this case is actually aggregate initialization because StructTest is an aggregate. This is allowed because the presence of an explicitly defaulted or deleted constructor still makes the class an aggregate.

2, Compiles. Default constructor for A and list init A1?, and the default copy constructor for A2

A1 is aggregate initialized like what happened in 1. The rest it correct

1 + 3 or 2 + 3, Fails to compile because of the deleted copy constructor for A2

This is the expected behavior since the copy constructor is marked as deleted.

1 + 4, Compiles. Default constructor for A and list init A1?, and the default copy constructor for A2

Again, aggregate initialization for A and A1

2 + 4, Compiles. Default constructor for A and list init A1?, and the default copy constructor for A2

A and A1 will be aggregate initialized but it will use the default member initializer of Var when initializing A per [dcl.init.aggr]/5.1

1 + 5, Fails to compile. Says A is missing(deleted) a default constructor, and no matching constructor for A1?

5 is a user provided non defaulted or deleted constructor. This means StructTest is no longer an aggregate and you cannot aggregate initialize it anymore.

2 + 5, Fails to compile. No matching constructor for A1?

Same reason as 1 + 5

C++14 value-initialization with deleted constructor

Your struct A is :

  • a class type that has:
    • no user-provided constructors1,
    • no private or protected non-static data members,
    • no base classes,
    • no virtual member functions.

It therefore qualifies as an aggregate type, according to the definition provided by § 8.5.1/1.

Then comes the priority of aggregate initialization over value initialization. The standard says that aggregate initialization has precedence over value intialization (draft N3936, § 8.5.4/3, page 201) (emphasis mine)

List-initialization of an object or reference of type T is defined as follows:

  • If T is an aggregate, aggregate initialization is performed (8.5.1).
  • Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
  • [... more rules...]

(1) As requested in the comments on why a deleted constructor does not count as user-defined, here is what the standard says (draft N3936, § 8.4.2/5, page 198):

A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration.

Why is the defaulted default constructor deleted for a union or union-like class?

This was changed between C++14 and C++17, via CWG 2084, which added the language allowing an NSDMI on (any) union member to restore the defaulted default constructor.

The example accompanying CWG 2084 though is subtly different to yours:

struct S {
S();
};
union U {
S s{};
} u;

Here the NSDMI is on the non-trivial member, whereas the wording adopted for C++17 allows an NSDMI on any member to restore the defaulted default constructor. This is because, as written in that DR,

An NSDMI is basically syntactic sugar for a mem-initializer

That is, the NSDMI on int b = 0; is basically equivalent to writing a constructor with mem-initializer and empty body:

C() : b{/*but use copy-initialization*/ 0} {}

As an aside, the rule ensuring that at most one variant member of the union has an NSDMI is somewhat hidden in a subclause of class.union.anon:

4 - [...] At most one variant member of a union may have a default member initializer.

My supposition would be that since gcc and Clang already allow the above (the NSDMI on the non-trivial union member) they didn't realize that they need to change their implementation for full C++17 support.

This was discussed on the list std-discussion in 2016, with an example very similar to yours:

struct S {
S();
};
union U {
S s;
int i = 1;
} u;

The conclusion was that clang and gcc are defective in rejecting, although there was at the time a misleading note, amended as a result.

For Clang, the bug is https://bugs.llvm.org/show_bug.cgi?id=39686 which loops us back to SO at Implicitly defined constructor deleted due to variant member, N3690/N4140 vs N4659/N4727. I can't find a corresponding bug for gcc.

Note that MSVC correctly accepts, and initializes c to .b = 0, which is correct per dcl.init.aggr:

5 - [...] If the aggregate is a union and the initializer list is empty, then

  • 5.4 - if any variant member has a default member initializer, that member is initialized from its default member initializer; [...]


Related Topics



Leave a reply



Submit