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.
The {}
calls the default constructor.
... Except when:
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:
Then it fails with call to deleted constructor
.
...Except when:
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:
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?
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.
struct foo {
foo() = delete;
foo(int) = delete;
};
foo f{}; // OK
struct foo {
foo() = delete;
foo(int) {};
};
foo f{}; // error call to deleted constructor
struct foo {
int a;
foo() = delete;
};
foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK
struct foo {
int a;
foo() = delete;
foo(int) = delete;
};
foo f{}; // ERROR: missing initializer
foo f{3}; // OK
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:
foo
is an aggregatefoo
is not an aggregate4.9
doesn't implement this.5.2.0
does5.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):
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:
Many thanks to @M.M who helped correct and improve this post.