用作初始值设定项时,空括号(“T()”)和空大括号(“T{}”)有什么区别吗?

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

一般来说,圆括号和大括号的区别很大。对于最小的可重现示例:

#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
}

但是,由于空括号使用值初始化而不是 std::initializer_list 构造函数,空括号和空括号在用作初始化器时有什么不同吗?

更正式地说,给定类型 TT()T{} 是否可能不同? (两者都可能格式不正确。)

(此问题和答案最初是为 C++20 standard compatible vector 在 Code Review Stack Exchange 上创建的。旨在回答涵盖所有可能的情况。如有遗漏,请通知我。)

(此答案中的链接指向 N4659,C++17 最终草案。但是,在撰写本文时,C++20 的情况完全相同。)

是的,这是可能的。有两种情况:

案例一

T 是一个非联合聚合,其零初始化,如果聚合具有非平凡的构造函数,则进行默认初始化,不同于 [= 的复制初始化14=].

我们可以使用std::in_place_t来构造我们的例子,因为它有一个明确的默认构造函数。最小可重现示例:

#include <utility>

struct A {
    std::in_place_t x;
};

int main()
{
    A(); // well-formed
    A{}; // ill-formed
}

(live demo)

案例 1,变体

T 是一个联合聚合,其第一个元素的默认初始化不同于 {} 的复制初始化。

我们可以将案例 1 中的 struct 更改为 union 以形成一个最小的可重现示例:

#include <utility>

union A {
    std::in_place_t x;
};

int main()
{
    A(); // well-formed
    A{}; // ill-formed
}

(live demo)

案例二

Tconst U&U&& 的形式,其中 U 可以从 {}.

最小可重现示例:

int main()
{
    using R = const int&;
    R{}; // well-formed
    R(); // ill-formed
}

(live demo)

详细解释

T()

根据 [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.

  • [...]

我们可以得出结论,T() 总是对对象进行值初始化。

T{}

根据 [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.

  • [...]

这足以让我们得出结论 T{} 总是列表初始化对象。

现在让我们来看看[dcl.init.list]/3。我已经突出显示可能的情况。其他情况是不可能的,因为它们要求初始化列表是非空的。

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.

(注意:(3.6) 在这种情况下是不可能的,原因如下:(3.4) 涵盖了存在默认构造函数的情况。为了考虑 (3.6),非默认构造函数必须调用构造函数,这对于空的初始化列表是不可能的。(3.11) 是不可能的,因为 (3.10) 涵盖了所有情况。)

下面我们来分析案例:

(3.3)

对于聚合,值初始化首先执行零初始化,然后,如果元素具有非平凡的默认构造函数,默认初始化,在聚合上,根据 [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;

  • [...]

非工会总计

当从 {} 复制初始化非联合聚合时,未使用默认成员初始值设定项显式初始化的元素将从 {} 复制初始化每个 [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.

[...]

见案例一

联盟总计

如果聚合是联合,并且没有成员有默认的成员初始值设定项,则从 {} 复制初始化聚合会复制初始化 first 元素{}: [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.

参见案例 1,变体。

(3.4)

值已初始化,所以没有区别。

(3.9)

如果 T 是每个 [dcl.init]/9 的参考:

,则不允许

T()

A program that calls for default-initialization or value-initialization of an entity of reference type is ill-formed.

参见案例 2。

(3.10)

同理,值初始化。没有区别。

is there any different between empty parentheses and empty braces when used as initializers

有些情况下空括号不能用作初始化器,因为它在语法上与函数声明不明确:

T t(); // function declaration; not initialisation
T t{}; // value initialisation

More formally, given a type T, is it possible that T() and T{} are different?

上面描述的歧义有一种情况,其中T()被解析为指向函数的指针,称为Most Vexing Parse:

U t(T()); // function declaration; not initialisation
U t(T{}); // value initialisation, and direct initialisation