为什么 C++ 允许 std::initializer_list 被强制转换为基本类型,并用于初始化它们?

Why does C++ allow std::initializer_list to be coerced to primitive types, and be used to initialise them?

这个问题是关于std::initializer_list,以及为什么允许初始化原始类型。考虑以下两个函数:

void foo(std::string arg1, bool arg2 = false);
void foo(std::string arg1, std::deque<std::string> arg2, bool arg3 = false);

为什么这样调用 foo 时:

foo("some string", { });

选择了第一个重载,而不是第二个?好吧,实际上不是为什么它被选中,这是因为{ }可以用来初始化任何东西,包括原始类型。我的问题是这背后的原因。

std::initializer_list 采用 { args... },因此在编译时不能有不确定的长度。尝试做类似 bool b = { true, true } 的事情会得到 error: scalar object 'b' requires one element in initialiser.

虽然允许统一初始化似乎是个好主意,但事实是这是令人困惑且完全出乎意料的行为。事实上,编译器如何能够做到这一点,而不需要在后台做一些魔术 std::initializer_list 事情?

除非{ args... }是一个C++词法结构,在这种情况下我的观点仍然成立:为什么它被允许用于原始类型的初始化?

谢谢。在意识到调用了错误的重载之前,我已经完全 bug-hunting session 了。花了 10 分钟找出原因。

不幸的是,{} 实际上并不表示 std::initializer_list。它还用于统一初始化。统一初始化旨在解决 C++ 对象初始化的不同方式的一堆问题,但最终只会让事情变得更糟,并且与 std::initializer_list 的句法冲突相当糟糕。

底线是 {} 表示 std::initializer_list{} 表示统一初始化是两个不同的东西,除非它们不是。

Indeed, how is the compiler able to do this, without some magic in the background doing std::initialiser_list things?

上述魔法绝对存在。 { args... } 只是一个词法结构,语义解释取决于上下文——它肯定不是 std::initializer_list,除非上下文说它是。

why is it allowed to be used in the initialisation of primitive types?

因为标准委员会没有正确考虑对两个功能使用相同的语法有多糟糕。

最终,统一初始化被设计破坏,实际上应该被禁止。

{} 语法是 braced-init-list,并且由于它在函数调用中用作参数,因此 复制-list-initializes一个对应的参数。

§ 8.5 [dcl.init]/p17:

(17.1) — If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).

§ 8.5.4 [dcl.init.list]/p1:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; [...]

对于 class 类型的参数,使用列表初始化,重载决策分两个阶段查找可行的构造函数:

§ 13.3.1.7 [over.match.list]/p1:

When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:

— Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.

— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

但是:

If the initializer list has no elements and T has a default constructor, the first phase is omitted.

由于 std::deque<T> 定义了一个非显式默认构造函数,因此将一个构造函数添加到一组可行的函数中以进行重载解析。通过构造函数的初始化被 class 化为 用户定义的转换 (§ 13.3.3.1.5 [over.ics.list]/p4):

Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion.

更进一步,一个空的花括号初始化列表可以对其相应的参数进行值初始化(§ 8.5.4 [dcl.init.list]/p3),对于文字类型来说代表零初始化:

(3.7) — Otherwise, if the initializer list has no elements, the object is value-initialized.

对于像 bool 这样的文字类型,这不需要任何转换,并且 class 化为 标准转换 (§ 13.3.3.1 .5 [over.ics.list]/p7):

Otherwise, if the parameter type is not a class:

(7.2) — if the initializer list has no elements, the implicit conversion sequence is the identity conversion.

[ Example:

void f(int);
f( { } );
// OK: identity conversion

end example ]

重载决议首先检查是否存在一个参数,其对应参数的转换序列优于另一个重载(§ 13.3.3 [over.match.best]/p1):

[...] Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then:

(1.3) — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that, [...]

转换序列按照 § 13.3.3.2 [over.ics.rank]/p2:

排序

When comparing the basic forms of implicit conversion sequences (as defined in 13.3.3.1)

(2.1) — a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and [...]

因此,第一个用 bool{} 初始化的重载被认为是更好的匹配。

My question is the reasoning behind this.

其背后的原因很简单(尽管有缺陷)。列表初始化初始化一切。
特别地,{}代表"default"初始化它对应的对象;这是否意味着它的 initializer_list 构造函数是用一个空列表调用的,或者它的默认构造函数被调用了,或者它是值初始化的,或者所有聚合子对象都是用 {} 初始化的等是无关紧要的:它应该充当上述可以应用的任何对象的通用初始值设定项。

如果你想调用第二个重载,你必须通过例如std::deque<std::string>{}(或首先传递三个参数)。这就是目前的作案手法。

While it might have seemed like a good idea to allow uniform initialisation, the fact is that this is confusing and entirely unexpected behaviour.

无论如何我都不会称它为 "entirely unexpected"。列表初始化原始类型有什么令人困惑的地方?这对于聚合来说绝对是至关重要的——但是从聚合类型到算术类型并没有那么大的进步,因为在这两种情况下都没有涉及 initializer_list 。不要忘记它可以,例如也有助于防止变窄。

std::initialiser_list takes { args... }, and as such cannot have indeterminate length at the time of compilation.

嗯,从技术上讲,

std::initializer_list<int> f(bool b) {
    return b? std::initializer_list<int>{} : std::initializer_list<int>{1};
}