Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

The naive, optimistic and oh.. so wrong view of the c++11 uniform initialization syntax

I thought that since C++11 user-defined type objects should be constructed with the new {...} syntax instead of the old (...) syntax (except for constructor overloaded for std::initializer_list and similar parameters (e.g. std::vector: size ctor vs 1 elem init_list ctor)).

The benefits are: no narrow implicit conversions, no problem with the most vexing parse, consistency(?). I saw no problem as I thought they are the same (except the example given).

But they are not.

A tale of pure madness

The {} calls the default constructor.

... Except when:

  • the default constructor is deleted and
  • there are no other constructors defined.

Then it looks like it it rather value initializes the object?... Even if the object has deleted default constructor, the {} can create an object. Doesn't this beat the whole purpose of a deleted constructor?

...Except when:

  • the object has a deleted default constructor and
  • other constructor(s) defined.

Then it fails with call to deleted constructor.

...Except when:

  • the object has a deleted constructor and
  • no other constructor defined and
  • at least a non-static data member.

Then it fails with missing field initializers.

But then you can use {value} to construct the object.

Ok maybe this is the same as the first exception (value init the object)

...Except when:

  • the class has a deleted constructor
  • and at least one data members in-class default initialized.

Then nor {} nor {value} can create an object.

I am sure I missed a few. The irony is that it is called uniform initialization syntax. I say again: UNIFORM initialization syntax.

What is this madness?

Scenario A

Deleted default constructor:

struct foo {   foo() = delete; };  // All bellow OK (no errors, no warnings) foo f = foo{}; foo f = {}; foo f{}; // will use only this from now on. 

Scenario B

Deleted default constructor, other constructors deleted

struct foo {   foo() = delete;   foo(int) = delete; };  foo f{}; // OK 

Scenario C

Deleted default constructor, other constructors defined

struct foo {   foo() = delete;   foo(int) {}; };  foo f{}; // error call to deleted constructor 

Scenario D

Deleted default constructor, no other constructors defined, data member

struct foo {   int a;   foo() = delete; };  foo f{}; // error use of deleted function foo::foo() foo f{3}; // OK 

Scenario E

Deleted default constructor, deleted T constructor, T data member

struct foo {   int a;   foo() = delete;   foo(int) = delete; };  foo f{}; // ERROR: missing initializer foo f{3}; // OK 

Scenario F

Deleted default constructor, in-class data member initializers

struct foo {   int a = 3;   foo() = delete; };  /* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()` /* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)` 

like image 316
bolov Avatar asked Nov 29 '15 21:11

bolov


People also ask

Can default constructor be deleted?

Deleted implicitly-declared default constructor T has a member (without a default member initializer) (since C++11) which has a deleted default constructor, or its default constructor is ambiguous or inaccessible from this constructor.

Is default constructor always created?

No, the C++ compiler doesn't create a default constructor when we initialize our own, the compiler by default creates a default constructor for every class; But, if we define our own constructor, the compiler doesn't create the default constructor.

Can an object created by using default constructor?

Yes, a constructor can contain default argument with default values for an object.

What will happen when copy constructor makes equal to delete?

The copy constructor and copy-assignment operator are public but deleted. It is a compile-time error to define or call a deleted function. The intent is clear to anyone who understands =default and =delete . You don't have to understand the rules for automatic generation of special member functions.


2 Answers

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.

like image 197
bolov Avatar answered Oct 09 '22 12:10

bolov


What's messing you up is aggregate initialization.

As you say, there are benefits and drawbacks to using list initialization. (The term "uniform initialization" is not used by the C++ Standard).

One of the drawbacks is that list initialization behaves differently for aggregates than non-aggregates. Also, the definition of aggregate changes slightly with each Standard.


Aggregates are not created via a constructor. (Technically they actually might be, but this is a good way to think of it). Instead, when creating an aggregate, memory is allocated and then each member is initialized in order according to what's in the list initializer.

Non-aggregates are created via constructors, and in that case the members of the list initializer are constructor arguments.

There is actually a design flaw in the above: if we have T t1; T t2{t1};, then the intent is to perform copy-construction. However, (prior to C++14) if T is an aggregate then aggregate initialization happens instead, and t2's first member is initialized with t1.

This flaw was fixed in a defect report which modified C++14, so from now on, copy-construction is checked for before we move onto aggregate initialization.


The definition of aggregate from C++14 is:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

In C++11, a default value for a non-static member meant a class was not an aggregate; however that was changed for C++14. User-provided means user-declared , but not = default or = delete.


If you want to make sure that your constructor call never accidentally performs aggregate initialization, then you have to use ( ) rather than { }, and avoid MVPs in other ways.

like image 38
M.M Avatar answered Oct 09 '22 11:10

M.M