使用空初始值设定项列表直接初始化

Direct initialization with empty initializer list

本文关键字:列表 初始化      更新时间:2023-10-16
struct X
{
X() { std::cout << "default ctor" << std::endl; }
};
int main()
{
X({});
}

这打印出来

default ctor

这是有道理的,因为空大括号值初始化对象(我认为)。 然而

struct X
{
X() { std::cout << "default ctor" << std::endl; }
X(std::initializer_list<int>) { std::cout << "initializer list" << std::endl; }
};
int main()
{
X({});
}

为此,我得到了

initializer list

我不觉得这种行为很奇怪,但我并不完全相信。这有什么规则?

这种行为是否写在标准的某个部分?

若要查看实际情况,请声明 copy 和 move 构造函数,在 C++14 模式或更早模式下编译,并禁用复制 elision。

科里鲁链接

输出:

default ctor
move ctor

在第一个代码段中,编译器查找采用单个参数的X构造函数,因为您提供了单个参数。这些是复制和移动构造函数,X::X(const X&)X::X(X&&),如果您自己不声明它们,编译器将为您隐式声明。然后,编译器使用默认构造函数将{}转换为X对象,并将该X对象传递给移动构造函数。(您必须使用fno-elide-constructors才能看到这一点,否则编译器将省略移动,并且在 C++17 中,复制省略成为强制性的。

在第二个代码段中,编译器现在可以选择将{}转换为X(然后调用移动构造函数)或将{}转换为std::initializer_list<int>(然后调用初始值设定项列表构造函数)。根据 [over.ics.list]/6.2,调用默认构造函数的从{}X的转换是用户定义的转换,而根据 [over.ics.list]/4,从{}std::initializer_list<int>的转换是标识转换。标识转换优于用户定义的转换,因此编译器调用初始值设定项列表构造函数。

这种行为是否写在标准的某个部分?

答案是肯定的。这一切都是由 [dcl.init]/16 中的规则决定的,强调我的以匹配您的初始值设定项:

初始值设定项的语义如下所示。目标类型为 正在初始化的对象或引用的类型以及源 type 是初始值设定项表达式的类型。如果初始值设定项是 不是单个(可能是括号)表达式,源类型是 未定义。

  • 如果初始值设定项是(非括号)大括号初始化列表,则对象或引用是列表初始化的([dcl.init.list])。

  • [...]

  • 如果目标类型是(可能符合 cv 条件的)类类型:

    • 如果初始化是直接初始化,或者如果是复制初始化,其中源的 cv 非限定版本 类型与 目标,考虑构造函数。适用的构造函数 被枚举([over.match.ctor]),并通过以下方式选择最佳 过载分辨率([over.match])。如此选择的构造函数是 调用以使用初始值设定项表达式初始化对象,或 表达式列表作为其参数。如果没有构造函数适用,或者 重载解析不明确,初始化格式不正确。
    • [...]

您提供一个带括号的空大括号 init-list,因此仅应用后面的项目符号。考虑了构造函数,在第一种情况下,我们最终从默认初始化的X进行复制初始化。在后一种情况下,选择initializer_listc'tor作为更好的匹配。选择此重载的规则在 [over.ics.list] 中指定:

当参数是初始值设定项列表 ([dcl.init.list]) 时,它不是 表达式和特殊规则适用于将其转换为参数 类型。

如果参数类型为 std::initializer_list 或"X 数组",并且 初始值设定项列表的所有元素都可以隐式转换 到 X,隐式转换序列是最差的转换 将列表的元素转换为 X 所必需的。此转换可以 是用户定义的转换,即使在调用 初始值设定项列表构造函数。

否则,如果参数是非聚合类 X 并且重载 分辨率每个 [over.match.list] 选择单个最佳构造函数 X 从 参数初始值设定项列表,隐式转换序列为 用户定义的转换序列。如果多个构造函数可行 但没有一个比其他的更好,隐式转换序列 是模棱两可的转换序列。用户定义的转换是 允许将初始值设定项列表元素转换为 构造函数参数类型,[over.best.ics中所述除外。