Hot.PxL
Hot.PxL

Reputation: 1990

C++ direct list initialization with deleted default constructor

I have the following class definition. I included a private x to make sure it is not an aggregate.

class A {
 private:
  int x;

 public:
  A() = delete;
  A(std::initializer_list<int>) { printf("init\n"); }
};

Now if I initialize this object with A a = A{}, it will say A::A() is deleted. I guess it is trying to call A::A() but it is deleted. If I comment out that line, so A::A() is automatically generated. Then if I run this code, I could see that it is calling A::A(std::initializer_list<int>)!

And more confusing, if I define A() = default, the initialization calls A::A() again.

Can anyone point me to the right direction of understading this behavior? Thanks!

I'm using G++ 6.3.0 with flag -std=c++17.

Upvotes: 3

Views: 1157

Answers (3)

Barry
Barry

Reputation: 304122

List initialization follows a very specific ordering, as laid out in [dcl.init.list]/3. The two relevant bullet points and their relative ordering are highlighted:

List-initialization of an object or reference of type T is defined as follows:
— If T is an aggregate class and [...]
— Otherwise, if T is a character array and [...]
— Otherwise, if T is an aggregate, [...]
Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
— Otherwise, if T is a specialization of std::initializer_list<E>, [...]
Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7).
— Otherwise, if the initializer list has a single element of type E and [...]
— Otherwise, if T is a reference type, [...]
— Otherwise, if T is an enumeration with a fixed underlying type (7.2), [...]
— Otherwise, if the initializer list has no elements, the object is value-initialized.
— Otherwise, the program is ill-formed.

An empty initializer list for class types with default constructors is a special case that precedes normal constructor resolution. Your class isn't an aggregate (since it has a user-provided constructor), so A{} will attempt to value-initialize A if A has a default constructor. It doesn't matter if that default constructor is accessible, it only matters if it exists. If it exists and is inaccessible (your first case), that's ill-formed. If it exists and is accessible (your third case), then it's invoked. Only if your class does not have a default constructor will the constructors be enumerated, in which case the A(initializer_list<int> ) constructor will be invoked with an empty initializer list (your second case).

Upvotes: 1

Praetorian
Praetorian

Reputation: 109289

You have lots of cases listed, so let us go over them one by one.

A() = delete;
A(std::initializer_list<int>) { ... }

Writing A a = A{}; will indeed try to call the deleted default constructor, and your code fails to compile.

What you have above is list initialization, and because the braced-init-list is empty, value initialization will be performed. This selects the deleted default constructor, which makes your code ill-formed.


If I comment out that line, so A::A() is automatically generated

No, this is not the case. There is no implicitly declared default constructor due to the presence of the user provided constructor that takes an initializer_list. Now, A a = A{}; will call the initializer_list constructor, and in this case the initializer_list will be empty.


if I define A() = default, the initialization calls A::A() again

This behaves the same as the first case, except the default constructor is explicitly defaulted instead of deleted, so your code compiles.


Finally,

I included a private x to make sure it is not an aggregate

There's no need for this, defining a constructor makes A a non-aggregate.

Upvotes: 2

MABVT
MABVT

Reputation: 1360

The behaviour is correct.

First of all:

// Calling
A a = A{}; // equals A a = A();, 

which was a convenience uniformization of the list-initialization idiom. Refer to: https://stackoverflow.com/a/9020606/3754223

Case A:

class A {
     private:
         int x;

     public:
         **A() = delete;**
         A(std::initializer_list<int>) { printf("init\n"); }
};

As said above, A a = A{} will be ... = A(), which is deleted.

Case B:

class A {
     private:
         int x;

     public:
         A(std::initializer_list<int>) { printf("init\n"); }
};

In this case, there's no default constructor provided, since you have your initializer-list constructor be defined. Consequently {} is converted to an empty initializer list implicitely!

Case C:

class A {
     private:
         int x;

     public:
         **A() = default;**
         A(std::initializer_list<int>) { printf("init\n"); }
};

In this case the empty default constructor is available and A{} is converted back to A(), causing it to be called.

Please refer to the below pages and get a thorough read into it. http://en.cppreference.com/w/cpp/utility/initializer_list http://en.cppreference.com/w/cpp/language/value_initialization

Finally:

Using:

A a = {{}};

Would cause that ALWAYS the initializer-list constructor is called, as the outer-curly-brackets denoted the init-list, and the inner a zero-constructed element. -> Non-empty initializer-list... (See stackoverflow link above again!)

And btw, I cannot understand why the question is downvoted, considering that this is a really tricky part...

Upvotes: 2

Related Questions