3

一般来说,括号和大括号是非常不同的。对于最小的可重现示例:

#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构造函数,所以在用作初始化程序时,空括号和空括号有什么不同吗?

更正式地说,给定一个 type T,是否有可能T()T{}不同?(两者都可能格式不正确。)

(这个问题和答案最初是为Code Review Stack Exchange 上的C++20 标准兼容向量创建的。它的目的是让答案涵盖所有可能的情况。如果我遗漏了任何情况,请通知我。)

4

2 回答 2

4

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

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

情况1

T是一个非联合聚合,其零初始化,如果聚合具有非平凡构造函数,则后跟默认初始化,不同于复制初始化{}

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

#include <utility>

struct A {
    std::in_place_t x;
};

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

现场演示

案例 1,变体

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

我们可以在案例 1 中更改structunion以形成一个最小的可重现示例:

#include <utility>

union A {
    std::in_place_t x;
};

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

现场演示

案例2

T是形式const U&或可以从 列表初始化的U&&地方。U{}

最小的可重现示例:

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

现场演示

详细解释

T()

根据[dcl.init]/17

初始化器的语义如下。目标类型 是正在初始化的对象或引用的 类型,源类型是初始化表达式的类型。如果初始化器不是单个(可能是带括号的)表达式,则未定义源类型。

  • 如果初始值设定项是(无括号的)花括号初始化列表或花括号= 初始化列表,则对象或引用是列表初始化的。

  • 如果目标类型是引用类型,请参见 [dcl.init.ref]。

  • 如果目标类型是字符数组、 的数组char16_­t、 的数组char32_­t或 的数组wchar_­t,并且初始值设定项是字符串文字,请参阅 [dcl.init.string]。

  • 如果初始化程序是(),则对象是值初始化的。

  • [...]

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

T{}

根据[dcl.init]/17

初始化器的语义如下。目标类型 是正在初始化的对象或引用的 类型,源类型是初始化表达式的类型。如果初始化器不是单个(可能是带括号的)表达式,则未定义源类型。

  • 如果初始值设定项是(无括号的)花括号初始化列表或花括号= 初始化列表,则对象或引用是列表初始化的。

  • [...]

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

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

类型的对象或引用的列表初始化T定义如下:

  • (3.1) 如果T是一个聚合类并且初始化器列表有一个类型为cv U的元素,其中UisT或派生自 的类 T,则从该元素初始化对象(通过复制列表初始化的复制初始化,或通过直接- 直接列表初始化的初始化)。

  • (3.2) 否则,如果T是一个字符数组并且初始化器列表有一个元素是一个适当类型的字符串字面量 ([dcl.init.string]),则按照该节中的描述执行初始化。

  • (3.3)否则,如果T是一个聚合,则执行聚合初始化。

  • (3.4)否则,如果初始化列表没有元素并且T是具有默认构造函数的类类型,则该对象被值初始化。

  • (3.5) 否则,如果T是 的特化 std​::​initializer_­list<E>,则按如下所述构造对象。

  • (3.6) 否则,如果T是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议([over.match]、[over.match.list])选择最佳构造函数。如果需要缩小转换(见下文)来转换任何参数,则程序格式错误。

  • (3.7) 否则,如果T是一个具有固定基础类型的枚举 ([dcl.enum]),则初始化器列表只有一个元素v,并且初始化为直接列表初始化,则使用值T(v)([ expr.type.conv]); 如果需要缩小转换以转换v为 的基础类型T,则程序格式错误。

  • (3.8) 否则,如果初始化列表有一个类型的元素E 并且T不是引用类型或其引用类型与 引用相关E,则从该元素初始化对象或引用(通过复制列表的复制初始化- 初始化,或通过直接列表初始化的直接初始化);如果需要缩小转换(见下文)将元素转换为 T,则程序格式错误。

  • (3.9)否则,如果T是引用类型,T则生成所引用类型的纯右值。prvalue 通过复制列表初始化或直接列表初始化来初始化其结果对象,具体取决于引用的初始化类型。然后使用纯右值直接初始化引用。

  • (3.10)否则,如果初始化列表没有元素,则对象被值初始化。

  • (3.11) 否则,程序是非良构的。

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

现在让我们分析案例:

(3.3)

对于聚合,值初始化首先执行零初始化,然后,如果元素具有非平凡的默认构造函数,则在聚合上执行默认初始化,每个[dcl.init]/8

对 T 类型的对象进行值初始化意味着:

  • [...]

  • 如果T是没有用户提供或删除的默认构造函数的(可能是 cv 限定的)类类型,则该对象被零初始化并检查默认初始化的语义约束,并且如果 T 具有非平凡的默认构造函数,该对象是默认初始化的

  • [...]

非工会聚集体

当从 复制初始化非联合聚合时{},未使用默认成员初始化器显式初始化的元素将从[dcl.init.aggr]/8{}复制​​初始化:

如果列表中的初始化子句少于非联合聚合中的元素,则每个未显式初始化的元素都按如下方式初始化:

  • 如果元素具有默认成员初始化程序 ([class.mem]),则从该初始化程序初始化元素。

  • 否则,如果元素不是引用,则从空的初始化列表 ([dcl.init.list]) 复制初始化该元素。

  • 否则,程序格式错误。

[...]

见案例 1。

联合聚合

如果聚合是联合,并且没有成员具有默认成员初始化程序,则从复制初始化聚合的{}复制初始化第一个元素{}[dcl.init.aggr]/8

[...]

如果聚合是联合并且初始化列表为空,则

  • 如果任何变体成员具有默认成员初始化器,则该成员从其默认成员初始化器初始化;

  • 否则,联合的第一个成员(如果有的话)从一个空的初始化列表复制初始化。

见案例 1,变体。

(3.4)

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

(3.9)

T()T如果是每个[dcl.init]/9的引用,则不允许:

要求对引用类型的实体进行默认初始化或值初始化的程序是格式错误的。

见案例 2。

(3.10)

同样,值初始化。没有不同。

于 2019-09-17T10:15:28.497 回答
1

用作初始化器时,空括号和空括号之间有什么不同吗

在某些情况下,空括号不能用作初始化器,因为它与函数声明在语法上会产生歧义:

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

更正式地说,给定类型 T,T() 和 T{} 是否可能不同?

上面描述的歧义有一个T()被解析为函数指针的情况,被称为最令人烦恼的解析:

U t(T()); // function declaration; not initialisation
U t(T{}); // value initialisation, and direct initialisation
于 2019-09-17T11:01:36.800 回答