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:
Case 1
Tis 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_tto construct our example, because it has an explicit default constructor. Minimal reproducible example:(live demo)
Case 1, variant
Tis a union aggregate for whose first element default-initialization differs from copy-initialization from{}.We can change
structtounionin Case 1 to form a minimal reproducible example:(live demo)
Case 2
Tis of the formconst U&orU&&whereUcan be list-initialized from{}.Minimal reproducible example:
(live demo)
Detailed explanation
T()Per [dcl.init]/17:
We can conclude that
T()always value-initializes the object.T{}Per [dcl.init]/17:
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.
(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:
(3.3)
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:
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: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:See Case 1, variant.
(3.4)
Value-initialized, so no difference.
(3.9)
T()isn't allowed ifTis a reference per [dcl.init]/9:See Case 2.
(3.10)
Similarly, value-initialized. No difference.