Generally speaking, parentheses and braces are very different. For minimal reproducible example:
#include <array>
#include <vector>
int main()
{
std::array<int, 2>{42, 42}; // OK
std::array<int, 2>(42, 42); // ill-formed
std::vector<int>{42, 42}; // two elements
std::vector<int>(42, 42); // 42 elements
}
However, since empty braces use value-initialization instead of std::initializer_list
constructors, is there any different between empty parentheses and empty braces when used as initializers?
More formally, given a type T
, is it possible that T()
and T{}
are different? (Either may be ill-formed.)
(This question and answer was originally created for C++20 standard compatible vector on Code Review Stack Exchange. It is intended that the answer covers all possible cases. Please inform me if I missed any.)
(The links in this answer point to N4659, the C++17 final draft. However, at the time of this writing, the situation is exactly the same for C++20.)
Yes, it's possible. There are two cases:
T
is a non-union aggregate for which zero-initialization, followed by default-initialization if the aggregate has a non-trivial constructor, differs from copy-initialization from {}
.
We can use std::in_place_t
to construct our example, because it has an explicit default constructor. Minimal reproducible example:
#include <utility>
struct A {
std::in_place_t x;
};
int main()
{
A(); // well-formed
A{}; // ill-formed
}
T
is a union aggregate for whose first element default-initialization differs from copy-initialization from {}
.
We can change struct
to union
in Case 1 to form a minimal reproducible example:
#include <utility>
union A {
std::in_place_t x;
};
int main()
{
A(); // well-formed
A{}; // ill-formed
}
T
is of the form const U&
or U&&
where U
can be list-initialized from {}
.
Minimal reproducible example:
int main()
{
using R = const int&;
R{}; // well-formed
R(); // ill-formed
}
T()
Per [dcl.init]/17:
The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.
If the initializer is a (non-parenthesized) braced-init-list or is
=
braced-init-list, the object or reference is list-initialized.If the destination type is a reference type, see [dcl.init.ref].
If the destination type is an array of characters, an array of
char16_t
, an array ofchar32_t
, or an array ofwchar_t
, and the initializer is a string literal, see [dcl.init.string].If the initializer is
()
, the object is value-initialized.[...]
We can conclude that T()
always value-initializes the object.
T{}
Per [dcl.init]/17:
The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.
If the initializer is a (non-parenthesized) braced-init-list or is
=
braced-init-list, the object or reference is list-initialized.[...]
That's enough for us to conclude that T{}
always list-initializes the object.
Now let's go through [dcl.init.list]/3. I have highlighted the possible cases. The other cases are not possible because they require the initializer list to be non-empty.
List-initialization of an object or reference of type
T
is defined as follows:
(3.1) If
T
is an aggregate class and the initializer list has a single element of type cvU
, whereU
isT
or a class derived fromT
, the object is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization).(3.2) Otherwise, if
T
is a character array and the initializer list has a single element that is an appropriately-typed string literal ([dcl.init.string]), initialization is performed as described in that section.(3.3) Otherwise, if
T
is an aggregate, aggregate initialization is performed.(3.4) Otherwise, if the initializer list has no elements and
T
is a class type with a default constructor, the object is value-initialized.(3.5) Otherwise, if
T
is a specialization ofstd::initializer_list<E>
, the object is constructed as described below.(3.6) Otherwise, if
T
is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.(3.7) Otherwise, if
T
is an enumeration with a fixed underlying type ([dcl.enum]), the initializer-list has a single elementv
, and the initialization is direct-list-initialization, the object is initialized with the valueT(v)
([expr.type.conv]); if a narrowing conversion is required to convertv
to the underlying type ofT
, the program is ill-formed.(3.8) Otherwise, if the initializer list has a single element of type
E
and eitherT
is not a reference type or its referenced type is reference-related toE
, the object or reference is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization); if a narrowing conversion (see below) is required to convert the element toT
, the program is ill-formed.(3.9) Otherwise, if
T
is a reference type, a prvalue of the type referenced byT
is generated. The prvalue initializes its result object by copy-list-initialization or direct-list-initialization, depending on the kind of initialization for the reference. The prvalue is then used to direct-initialize the reference.(3.10) Otherwise, if the initializer list has no elements, the object is value-initialized.
(3.11) Otherwise, the program is ill-formed.
(Note: (3.6) is not possible in this case, for the following reason: (3.4) covers the case where a default constructor is present. In order for (3.6) to be considered, a non-default constructor has to be called, which is not possible with an empty initializer list. (3.11) is not possible because (3.10) covers all cases.)
Now let's analyze the cases:
For an aggregate, value-initialization first performs zero-initialization and then, if the element has a non-trivial default constructor, default-initialization, on the aggregate, per [dcl.init]/8:
To value-initialize an object of type T means:
[...]
if
T
is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and if T has a non-trivial default constructor, the object is default-initialized;[...]
Non-union aggregates
When copy-initializing a non-union aggregate from {}
, elements that are not explicitly initialized with a default member initializer are copy-initialized from {}
per [dcl.init.aggr]/8:
If there are fewer initializer-clauses in the list than there are elements in a non-union aggregate, then each element not explicitly initialized is initialized as follows:
If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
Otherwise, the program is ill-formed.
[...]
See Case 1.
Union aggregates
If the aggregate is a union, and no member has a default member initializer, then copying-initializing the aggregate from {}
copy-initializes the first element from {}
: [dcl.init.aggr]/8:
[...]
If the aggregate is a union and the initializer list is empty, then
if any variant member has a default member initializer, that member is initialized from its default member initializer;
otherwise, the first member of the union (if any) is copy-initialized from an empty initializer list.
See Case 1, variant.
Value-initialized, so no difference.
T()
isn't allowed if T
is a reference per [dcl.init]/9:
A program that calls for default-initialization or value-initialization of an entity of reference type is ill-formed.
See Case 2.
Similarly, value-initialized. No difference.