c++initializationlanguage-lawyerlist-initializationvalue-initialization

Are there any difference in empty parentheses (“T()”) and empty braces (“T{}”) when used as initializers?


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.)


Solution

  • (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

    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
    }
    

    (live demo)

    Case 1, variant

    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
    }
    

    (live demo)

    Case 2

    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
    }
    

    (live demo)

    Detailed explanation

    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 of char32_­t, or an array of wchar_­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 cv U, where U is T or a class derived from T, 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 of std​::​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 element v, and the initialization is direct-list-initialization, the object is initialized with the value T(v) ([expr.type.conv]); if a narrowing conversion is required to convert v to the underlying type of T, the program is ill-formed.

    • (3.8) Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, 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 to T, the program is ill-formed.

    • (3.9) Otherwise, if T is a reference type, a prvalue of the type referenced by T 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:

    (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:

    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.

    (3.4)

    Value-initialized, so no difference.

    (3.9)

    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.

    (3.10)

    Similarly, value-initialized. No difference.