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)`
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.
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.
These cases around aggregate initialization are counter-intuitive for most and was the subject of the proposal p1008: Prohibit aggregates with user-declared constructors which says:
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. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform,
and easier to teach. We also discuss the breaking changes that this fix introduces
and introduces some examples, which overlap nicely with the cases you present:
struct X {
X() = delete;
};
int main() {
X x1; // ill-formed - default c’tor is deleted
X x2{}; // compiles!
}
Clearly, the intent of the deleted constructor is to prevent the user from initializing the class. However, contrary to intuition, this does not work: the user can still initialize X
via aggregate initialization because this completely bypasses the constructors. The author could even explicitly delete all of default, copy, and move constructor, and still fail to prevent the client code from instantiating X via aggregate initialization as above. Most C++ developers are surprised by the
current behaviour when shown this code
The author of class X could alternatively consider making the default constructor
private. But if
this constructor is given a defaulted definition, this again does not prevent aggregate initialization (and thus, instantiation) of the class:
struct X {
private:
X() = default;
};
int main() {
X x1; // ill-formed - default c’tor is private
X x2{}; // compiles!
}
Because of the current rules, aggregate initialization allows us to “default-construct” a class even if it is not, in fact, default-constructible:
static_assert(!std::is_default_constructible_v<X>);
would pass for both definitions of X above.
...
The proposed changes are:
Modify [dcl.init.aggr] paragraph 1 as follows:
An aggregate is an array or a class (Clause 12) with
no user-provided, explicit, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ or inherited
constructors (15.1),
no private or protected non-static data members (Clause 14),
no virtual functions (13.3), and
no virtual, private, or protected base classes (13.1).
Modify [dcl.init.aggr] paragraph 17 as follows:
[Note: An aggregate array or an aggregate class may contain elements of a class >>type with a user-provided u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ constructor (15.1). Initialization of >>these aggregate objects is described in 15.6.1. —end note]
Add the following to [diff.cpp17] in Annex C, section C.5 C++ and ISO C++ 2017:
C.5.6 Clause 11: declarators [diff.cpp17.dcl.decl]
Affected subclause: [dcl.init.aggr]
Change: A class that has
user-declared constructors is never an aggregate.
Rationale: Remove
potentially error-prone aggregate initialization which may apply
not withstanding the declared constructors of a class.
Effect on original feature: Valid C++ 2017 code that aggregate-initializes a
type with a user-declared constructor may be ill-formed or have
different semantics in this International Standard.
Followed by examples which I omit.
The proposal was accepted and merged into C++20 we can find the latest draft here which contains these changes and we can see the changes to [dcl.init.aggr]p1.1 and [dcl.init.aggr]p17 and C++17 declarations diff.
So this should be fixed in C++20 forward.
Related
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)`
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.
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.
These cases around aggregate initialization are counter-intuitive for most and was the subject of the proposal p1008: Prohibit aggregates with user-declared constructors which says:
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. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform,
and easier to teach. We also discuss the breaking changes that this fix introduces
and introduces some examples, which overlap nicely with the cases you present:
struct X {
X() = delete;
};
int main() {
X x1; // ill-formed - default c’tor is deleted
X x2{}; // compiles!
}
Clearly, the intent of the deleted constructor is to prevent the user from initializing the class. However, contrary to intuition, this does not work: the user can still initialize X
via aggregate initialization because this completely bypasses the constructors. The author could even explicitly delete all of default, copy, and move constructor, and still fail to prevent the client code from instantiating X via aggregate initialization as above. Most C++ developers are surprised by the
current behaviour when shown this code
The author of class X could alternatively consider making the default constructor
private. But if
this constructor is given a defaulted definition, this again does not prevent aggregate initialization (and thus, instantiation) of the class:
struct X {
private:
X() = default;
};
int main() {
X x1; // ill-formed - default c’tor is private
X x2{}; // compiles!
}
Because of the current rules, aggregate initialization allows us to “default-construct” a class even if it is not, in fact, default-constructible:
static_assert(!std::is_default_constructible_v<X>);
would pass for both definitions of X above.
...
The proposed changes are:
Modify [dcl.init.aggr] paragraph 1 as follows:
An aggregate is an array or a class (Clause 12) with
no user-provided, explicit, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ or inherited
constructors (15.1),
no private or protected non-static data members (Clause 14),
no virtual functions (13.3), and
no virtual, private, or protected base classes (13.1).
Modify [dcl.init.aggr] paragraph 17 as follows:
[Note: An aggregate array or an aggregate class may contain elements of a class >>type with a user-provided u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ constructor (15.1). Initialization of >>these aggregate objects is described in 15.6.1. —end note]
Add the following to [diff.cpp17] in Annex C, section C.5 C++ and ISO C++ 2017:
C.5.6 Clause 11: declarators [diff.cpp17.dcl.decl]
Affected subclause: [dcl.init.aggr]
Change: A class that has
user-declared constructors is never an aggregate.
Rationale: Remove
potentially error-prone aggregate initialization which may apply
not withstanding the declared constructors of a class.
Effect on original feature: Valid C++ 2017 code that aggregate-initializes a
type with a user-declared constructor may be ill-formed or have
different semantics in this International Standard.
Followed by examples which I omit.
The proposal was accepted and merged into C++20 we can find the latest draft here which contains these changes and we can see the changes to [dcl.init.aggr]p1.1 and [dcl.init.aggr]p17 and C++17 declarations diff.
So this should be fixed in C++20 forward.
Let's say I have a type and I want to make its default constructor private. I write the following:
class C {
C() = default;
};
int main() {
C c; // error: C::C() is private within this context (g++)
// error: calling a private constructor of class 'C' (clang++)
// error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
auto c2 = C(); // error: as above
}
Great.
But then, the constructor turns out to not be as private as I thought it was:
class C {
C() = default;
};
int main() {
C c{}; // OK on all compilers
auto c2 = C{}; // OK on all compilers
}
This strikes me as very surprising, unexpected, and explicitly undesired behavior. Why is this OK?
The trick is in C++14 8.4.2/5 [dcl.fct.def.default]:
... A function is user-provided if it is user-declared and not explicitly defaulted or
deleted on its first declaration. ...
Which means that C's default constructor is actually not user-provided, because it was explicitly defaulted on its first declaration. As such, C has no user-provided constructors and is therefore an aggregate per 8.5.1/1 [dcl.init.aggr]:
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).
You're not calling the default constructor, you're using aggregate initialization on an aggregate type. Aggregate types are allowed to have a defaulted constructor, so long as it's defaulted where it's first declared:
From [dcl.init.aggr]/1:
An aggregate is an array or a class (Clause [class]) with
no user-provided constructors ([class.ctor]) (including those inherited ([namespace.udecl]) from a base class),
no private or protected non-static data members (Clause [class.access]),
no virtual functions ([class.virtual]), and
no virtual, private, or protected base classes ([class.mi]).
and from [dcl.fct.def.default]/5
Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor] [class.dtor], [class.copy]), which might mean defining them as deleted. A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted; if such a function is implicitly defined as deleted, the program is ill-formed. [ Note: Declaring a function as defaulted after its first declaration can provide efficient execution and concise definition while enabling a stable binary interface to an evolving code base. — end note ]
Thus, our requirements for an aggregate are:
no non-public members
no virtual functions
no virtual or non-public base classes
no user-provided constructors inherited or otherwise, which allows only constructors which are:
implicitly declared, or
explicitly declared and defined as defaulted at the same time.
C fulfills all of these requirements.
Naturally, you may be rid of this false default construction behavior by simply providing an empty default constructor, or by defining the constructor as default after declaring it:
class C {
C(){}
};
// --or--
class C {
C();
};
inline C::C() = default;
Angew's and jaggedSpire's' answers are excellent and apply to c++11. And c++14. And c++17.
However, in c++20, things change a bit and the example in the OP will no longer compile:
class C {
C() = default;
};
C p; // always error
auto q = C(); // always error
C r{}; // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20
As pointed out by the two answers, the reason the latter two declarations work is because C is an aggregate and this is aggregate-initialization. However, as a result of P1008 (using a motivating example not too dissimilar from the OP), the definition of aggregate changes in C++20 to, from [dcl.init.aggr]/1:
An aggregate is an array or a class ([class]) with
no user-declared or inherited constructors ([class.ctor]),
no private or protected direct non-static data members ([class.access]),
no virtual functions ([class.virtual]), and
no virtual, private, or protected base classes ([class.mi]).
Emphasis mine. Now the requirement is no user-declared constructors, whereas it used to be (as both users cite in their answers and can be viewed historically for C++11, C++14, and C++17) no user-provided constructors. The default constructor for C is user-declared, but not user-provided, and hence ceases to be an aggregate in C++20.
Here is another illustrative example of aggregate changes:
class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
B was not an aggregate in C++11 or C++14 because it has a base class. As a result, B{} just invokes the default constructor (user-declared but not user-provided), which has access to A's protected default constructor.
In C++17, as a result of P0017, aggregates were extended to allow for base classes. B is an aggregate in C++17, which means that B{} is aggregate-initialization that has to initialize all the subobjects - including the A subobject. But because A's default constructor is protected, we don't have access to it, so this initialization is ill-formed.
In C++20, because of B's user-declared constructor, it again ceases to be an aggregate, so B{} reverts to invoking the default constructor and this is again well-formed initialization.
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)`
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.
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.
These cases around aggregate initialization are counter-intuitive for most and was the subject of the proposal p1008: Prohibit aggregates with user-declared constructors which says:
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. This paper proposes a fix that makes initialization semantics in C++ safer, more uniform,
and easier to teach. We also discuss the breaking changes that this fix introduces
and introduces some examples, which overlap nicely with the cases you present:
struct X {
X() = delete;
};
int main() {
X x1; // ill-formed - default c’tor is deleted
X x2{}; // compiles!
}
Clearly, the intent of the deleted constructor is to prevent the user from initializing the class. However, contrary to intuition, this does not work: the user can still initialize X
via aggregate initialization because this completely bypasses the constructors. The author could even explicitly delete all of default, copy, and move constructor, and still fail to prevent the client code from instantiating X via aggregate initialization as above. Most C++ developers are surprised by the
current behaviour when shown this code
The author of class X could alternatively consider making the default constructor
private. But if
this constructor is given a defaulted definition, this again does not prevent aggregate initialization (and thus, instantiation) of the class:
struct X {
private:
X() = default;
};
int main() {
X x1; // ill-formed - default c’tor is private
X x2{}; // compiles!
}
Because of the current rules, aggregate initialization allows us to “default-construct” a class even if it is not, in fact, default-constructible:
static_assert(!std::is_default_constructible_v<X>);
would pass for both definitions of X above.
...
The proposed changes are:
Modify [dcl.init.aggr] paragraph 1 as follows:
An aggregate is an array or a class (Clause 12) with
no user-provided, explicit, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ or inherited
constructors (15.1),
no private or protected non-static data members (Clause 14),
no virtual functions (13.3), and
no virtual, private, or protected base classes (13.1).
Modify [dcl.init.aggr] paragraph 17 as follows:
[Note: An aggregate array or an aggregate class may contain elements of a class >>type with a user-provided u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ constructor (15.1). Initialization of >>these aggregate objects is described in 15.6.1. —end note]
Add the following to [diff.cpp17] in Annex C, section C.5 C++ and ISO C++ 2017:
C.5.6 Clause 11: declarators [diff.cpp17.dcl.decl]
Affected subclause: [dcl.init.aggr]
Change: A class that has
user-declared constructors is never an aggregate.
Rationale: Remove
potentially error-prone aggregate initialization which may apply
not withstanding the declared constructors of a class.
Effect on original feature: Valid C++ 2017 code that aggregate-initializes a
type with a user-declared constructor may be ill-formed or have
different semantics in this International Standard.
Followed by examples which I omit.
The proposal was accepted and merged into C++20 we can find the latest draft here which contains these changes and we can see the changes to [dcl.init.aggr]p1.1 and [dcl.init.aggr]p17 and C++17 declarations diff.
So this should be fixed in C++20 forward.
In updating some code to use uniform initialization, I thought it would be a drop-in modern substitue for the now "old style" parentheses style. I know this isn't always the case (obvious example, vector<int>) but I've stumbled over another difference that I don't understand.
class Object {
public:
Object() = default;
Object(const Object&) = default;
};
int main() {
Object o;
Object copy{o}; // error
Object copy2(o); // OK
}
fails to compile under clang3.5 with the error: (also fails under gcc)
error: excess elements in struct initializer
There are two different changes to Object that make this work. Either adding a data member to it, or giving it an empty copy constructor body
class Object {
private:
int i; // this fixes it
public:
Object() = default;
Object(const Object&) { } // and/or this fixes it as well
};
I don't see why these should make a difference.
This is a known bug and will hopefully be fixed in C++17 (not for C++14, see http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1467). Your struct is an aggregate, so to initialize it with {someElement} there needs to be at least one data member, as you have discovered. Try providing an operator int(); and you will see it compiles.
Johannes's answer is useful, but let me elaborate on why it currently happens.
Both of the changes you describe affect your class by making it go from an aggregate to not an aggregate. See C++11 (N3485) § 8.5.1/1:
An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal-initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).
A constructor with a definition of = default is considered not user-defined.
Then, going down to list-initialization in § 8.5.4, we see:
List-initialization of an object or reference of type T is defined as follows:
If T is an aggregate, aggregate initialization is performed
And then a bunch of "Otherwise..." sections. Thus, changing either of these allows a constructor to be called instead of performing aggregate initialization.
The new proposed standardese for the list-initialization definition (as viewable in Johannes's link) provides a prioritized case of a single element in the list, and it having a type of (or really close to) the type of the object being initialized. Aggregate initialization would then be top priority after that.
In updating some code to use uniform initialization, I thought it would be a drop-in modern substitue for the now "old style" parentheses style. I know this isn't always the case (obvious example, vector<int>) but I've stumbled over another difference that I don't understand.
class Object {
public:
Object() = default;
Object(const Object&) = default;
};
int main() {
Object o;
Object copy{o}; // error
Object copy2(o); // OK
}
fails to compile under clang3.5 with the error: (also fails under gcc)
error: excess elements in struct initializer
There are two different changes to Object that make this work. Either adding a data member to it, or giving it an empty copy constructor body
class Object {
private:
int i; // this fixes it
public:
Object() = default;
Object(const Object&) { } // and/or this fixes it as well
};
I don't see why these should make a difference.
This is a known bug and will hopefully be fixed in C++17 (not for C++14, see http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1467). Your struct is an aggregate, so to initialize it with {someElement} there needs to be at least one data member, as you have discovered. Try providing an operator int(); and you will see it compiles.
Johannes's answer is useful, but let me elaborate on why it currently happens.
Both of the changes you describe affect your class by making it go from an aggregate to not an aggregate. See C++11 (N3485) § 8.5.1/1:
An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal-initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).
A constructor with a definition of = default is considered not user-defined.
Then, going down to list-initialization in § 8.5.4, we see:
List-initialization of an object or reference of type T is defined as follows:
If T is an aggregate, aggregate initialization is performed
And then a bunch of "Otherwise..." sections. Thus, changing either of these allows a constructor to be called instead of performing aggregate initialization.
The new proposed standardese for the list-initialization definition (as viewable in Johannes's link) provides a prioritized case of a single element in the list, and it having a type of (or really close to) the type of the object being initialized. Aggregate initialization would then be top priority after that.