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

时间:2015-08-08 16:48:36

标签: c++ c++11 initializer-list

这个问题与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 ++词法结构,否则我的观点仍然存在:为什么允许在原始类型的初始化中使用它?

感谢。在意识到错误的重载被调用之前,我在这里遇到了很多错误。花了10分钟搞清楚原因。

3 个答案:

答案 0 :(得分:5)

{}语法是 braced-init-list ,因为它在函数调用中用作参数,所以 copy-list-initializes 相应的参数。

§8.5[dcl.init] / p17:

  

(17.1) - 如果初始化程序是(非括号的) braced-init-list ,则对象或引用是列表初始化的(8.5.4)

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

  

列表初始化是从 braced-init-list 初始化对象或引用。这样的初始化程序是   称为初始化列表,列表中逗号分隔的初始化子句称为元素   初始化列表。初始化列表可以为空。 列表初始化可以在直接初始化复制初始化上下文中进行; [...]

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

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

  

当非聚合类类型T的对象被列表初始化(8.5.4)时,重载决策选择构造函数   分两个阶段:

     

- 最初,候选函数是类T的初始化列表构造函数(8.5.4),参数列表由初始化列表作为单个参数组成。

     

- 如果找不到可行的初始化列表构造函数,则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始化列表的元素组成。 / p>

但:

  

如果初始化列表没有元素且T有默认构造函数,则省略第一阶段。

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

  

否则,如果参数是非聚合类X,并且每13.3.1.7的重载决策选择一个   X的最佳构造函数,用于从参数初始化列表中执行类型X的对象的初始化,   隐式转换序列是用户定义的转换序列,具有第二个标准转换   序列身份转换。

更进一步,空的braced-init-list可以初始化其相应的参数(第8.5.4节[dcl.init.list] / p3),对于文字类型代表零初始化:

  

(3.7) - 否则,如果初始化列表没有元素,则对象进行值初始化。

对于像bool这样的字面类型,这不需要任何转换,并归类为标准转换(第13.3.3.1.5节[over.ics.list] / P7):

  

否则,如果参数类型不是类:

     

(7.2) - 如果初始化列表没有元素,则隐式转换序列是标识转换。

     

[示例

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

- 结束示例]

如果存在对应参数的转换序列优于另一个重载的参数(第13.3.3节[over.match.best] / p1),则首先进行重载分辨率检查:

  

[...]鉴于这些定义,可行函数F1被定义为比另一个可行函数更好的函数   F2如果对于所有参数iICSi(F1)的转换序列不是ICSi(F2),那么:

     

(1.3) - 对于某些参数jICSj(F1)是比ICSj(F2)更好的转换序列,或者,如果不是,[...] ]

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

  

比较隐式转换序列的基本形式(如13.3.3.1中所定义)

     

(2.1) - 标准转换序列(13.3.3.1.1)是比用户定义的转换序列或省略号转换序列更好的转换序列,并且[...] < / p>

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

答案 1 :(得分:3)

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

底线是{}表示std::initializer_list{}表示统一初始化是两回事,除非它们不是。

  

确实,编译器如何能够做到这一点,而没有一些魔术   做std :: initialiser_list事情的背景?

前面提到的魔法最确定存在。 { args... }只是一个词汇结构,语义解释依赖于语境 - 它肯定不是std::initializer_list,除非语境是这样的。

  

为什么允许在原始类型的初始化中使用它?

因为标准委员会没有正确考虑对两种功能使用相同的语法有多么破碎。

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

答案 2 :(得分:0)

  

我的问题是这背后的原因。

背后的原因很简单(虽然有缺陷)。列表初始化初始化所有内容 特别是,{}代表&#34;默认&#34;初始化它对应的对象;这是否意味着使用空列表调用其initializer_list - 构造函数,或者调用其默认构造函数,或者是否已对其进行值初始化,或者是否使用{}初始化所有聚合子对象等等是无关紧要的:它应该作为上述可以应用的任何对象的通用初始化器。

如果你想拨打第二个超载,你必须通过例如std::deque<std::string>{}(或者首先传递三个参数)。这是当前的运作方式。

  

虽然允许制服似乎是个好主意   初始化,事实是,这是令人困惑和完全   出乎意料的行为。

我不会把它称为“完全出乎意料的”#34;以任何方式。列表初始化基元类型有什么困惑?它对于聚合来说绝对至关重要 - 但是从聚合类型到算术类型并没有那么大的步骤,因为在这两种情况下都不涉及initializer_list。不要忘记它可以例如有助于防止缩小。

  

std::initialiser_list需要{ args... },因此不能拥有   编译时的长度不确定。

从技术上讲,

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