std::initializer_list的变化

5
以下三种使用 std::initializer_list 的初始化方式有什么不同?
std::vector<int> a{ 2, 3, 5, 7};
std::vector<int> b( { 2, 3, 5, 7} );
std::vector<int> c = { 2, 3, 5, 7};

在上面的示例中,std::vector只是一个占位符,但我对一个通用答案感兴趣。

这可能取决于a、b、c的类型,因此给它们一些意义是有意义的。 - PlasmaHH
3
这个问题是关于std::vector还是关于初始化的一般性问题? - David Rodríguez - dribeas
1
我们确定 std::vector<int> c = std::vector<int>{ 2, 3, 5, 7}; 与问题中的不同,是否应该添加?我不确定。 - Mooing Duck
4个回答

2
在上面的例子中,std::vector只是一个占位符,我对一个通用答案感兴趣。
你想要多通用的答案?因为这实际上取决于你正在初始化的类型以及它们具有什么构造函数。
例如:
T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );

这些可能是两个不同的东西,也可能不是。这取决于构造函数 T 的情况。如果 T 有一个接受单个 initializer_list<int>(或其他一些 initializer_list<U>,其中 U 是整数类型)的构造函数,则这两个都将调用该构造函数。
然而,如果没有这个构造函数,则这两个将执行不同的操作。第一个将尝试调用一个接受可以由整数字面值生成的 4 个参数的构造函数。第二个将尝试调用一个接受一个参数的构造函数,并尝试使用 {2, 3, 5, 7} 初始化它。这意味着它将遍历每个一参数构造函数,找出该参数的类型,并尝试使用 R{2, 3, 5, 7} 构造它。如果没有一个有效,则尝试将其作为 initializer_list<int> 传递。如果还不行,则失败。 initializer_list 构造函数始终具有优先级。
请注意,只有因为 {2, 3, 5, 7} 是一个括号初始化列表,其中每个元素具有相同的类型,所以 initializer_list 构造函数才起作用。如果你有 {2, 3, 5.3, 7.9},那么它不会检查 initializer_list 构造函数。
T c = { 2, 3, 5, 7};

这将表现得像 a,除了它所做的转换类型。由于这是复制列表初始化,它将尝试调用一个initializer_list构造函数。如果没有这样的构造函数可用,它将尝试调用一个4个参数的构造函数,但它只允许其for参数隐式转换为类型参数。
这就是唯一的区别。它不需要复制/移动构造函数或任何其他东西(规范仅在3个地方提到复制列表初始化。当复制/移动构造不可用时,没有任何禁止)。它几乎与a完全相同,除了它允许在其参数上进行的转换类型。
这就是为什么它通常被称为“统一初始化”的原因:因为它在几乎所有地方都以几乎相同的方式工作。

如果你有 {2, 3, 5.3, 7.9},那么它就不会检查初始化列表构造函数。这是不正确的。它会考虑它们,并且参数的转换序列将是任何元素的最差转换序列。 - Johannes Schaub - litb

2

让我们抽象化 std::vector,并称之为 T

T t{a, b, c};
T t = { a, b, c };
T t({a, b, c});

前两种形式是列表初始化(它们之间唯一的区别在于,如果T是一个类,则第二个explicit构造函数不允许被调用。如果调用了一个,程序就会变得非法)。最后一种形式只是我们从C++03中熟知的普通直接初始化:

T t(arg);

出现{a, b, c}作为arg的意思是构造函数调用的参数是花括号初始化列表。这种第三种形式没有列表初始化的特殊处理。T必须是一个类类型,即使花括号初始化列表只有一个参数。我很高兴在发布C ++ 11之前在这个问题上我们制定了明确的规则。我们制定了明确的规则


至于第三个构造函数调用哪些构造函数,让我们假设

struct T {
  T(int);
  T(std::initializer_list<int>);
};

T t({1});

由于直接初始化只是对重载构造函数的调用,因此我们可以将其转换为

void ctor(int); 
void ctor(std::initializer_list<int>);
void ctor(T const&);
void ctor(T &&);

我们可以使用尾随函数,但如果选择这些函数,则需要用户定义的转换。为了初始化T ref参数,将使用列表初始化,因为这不是带圆括号的直接初始化(因此参数初始化相当于T ref t = { 1 })。前两个函数是完全匹配的。然而,标准规定,在这种情况下,当一个函数转换为std::initializer_list<T>而另一个函数没有时,前者函数获胜。因此,在这种情况下,将使用第二个ctor请注意,在这种情况下,我们不会进行两阶段重载解析,只有列表初始化才能做到这一点
对于前两个函数,我们将使用列表初始化,它将执行上下文相关的操作。如果T是一个数组,它将初始化一个数组。以这个类为例:
struct T {
  T(long);
  T(std::initializer_list<int>);
};

T t = { 1L };

在这种情况下,我们进行两阶段重载解析。我们首先只考虑初始化列表构造函数,并查看是否有一个匹配项,作为参数我们采用整个大括号初始化列表。第二个构造函数匹配,所以我们选择它。我们将忽略第一个构造函数。如果我们没有初始化列表构造函数或者没有任何匹配项,我们将采用所有构造函数和初始化列表的元素。
struct T {
  T(long);

  template<typename A = std::initializer_list<int>>
  T(A);
};

T t = { 1L };

在这种情况下,我们选择第一个构造函数,因为1L无法转换为std::initializer_list<int>

如果我们有T t({});会怎样?优先顺序(如果我可以这么说)仍然是ctor(std::initializer_list<int>)ctor(int)ctor(T ref)吗?后两个候选项(使用T)是否仍需要用户定义的转换? - Luc Danton
@luc 是的,没有任何变化。 - Johannes Schaub - litb

2
传统上(C++98/03),像 T x(T()) 这样的初始化调用直接初始化,而像 T x = T() 这样的初始化则调用复制初始化。当使用复制初始化时,即使可能不需要,复制构造函数也必须存在并可用。
初始化列表有点改变了这一点。查看 § 8.5/14 和 § 8.5/15 可以发现术语“直接初始化”和“复制初始化”仍然适用,但是查看 § 8.5/16,我们发现对于花括号初始化列表,至少对于您的第一个和第三个示例来说,这是一个没有区别的区分:
— 如果初始化程序是(非括号)花括号初始化列表,则对象或引用被列表初始化(8.5.4)。
因此,您的第一个和第三个示例的实际初始化是相同的,并且两者都不需要复制构造函数(或移动构造函数)。在这两种情况下,我们处理§ 8.5.4 /3 中的第四个项目:
— 否则,如果 T 是类类型,则考虑构造函数。逐个列举适用的构造函数,通过重载决议(13.3、13.3.1.7)选择最佳构造函数。如果需要缩小转换(见下文)以转换任何参数,则程序无效。
… 因此,两者均使用带有 std::initializer_list 参数的 std::vector 的构造函数。
但是,正如上面引用中所指出的,这仅涉及“(非括号)花括号初始化列表”。对于使用带括号的初始化程序的第二个示例,我们进入 § 8.5/16 的第六个项目的第一个子项(天啊 - 真的需要与某人谈论在其中添加数字):
— 如果初始化是直接初始化,或者如果它是复制初始化,并且源类型的 cv-未限定版本是目标类的相同类或派生类,则考虑构造函数。列举适用的构造函数(13.3.1.3),然后通过重载决议(13.3)选择最佳构造函数。所选的构造函数使用初始化表达式或表达式列表作为其参数来初始化对象。如果没有构造函数适用,或者重载决议不明确,则初始化无效。
由于这使用直接初始化的语法,而括号内的表达式是花括号初始化程序列表,并且 std::vector 具有接受初始化程序列表的构造函数,因此选择该重载。

总之,尽管通过标准的路线不同,但这三种方式最终都使用了std::vector的构造函数重载来创建std::initializer_list<T>。从任何实用的角度来看,这三种方式没有区别。所有三种方式都会调用vector::vector(std::initializer_list<T>,而且没有复制或其他转换发生(即使是理论上可能被省略的转换也没有发生)。

然而,我认为在稍微不同的值下,可能会有一些小差异。缩小转换的禁止在§8.5.4/3中规定,因此第二个例子(它没有经过§8.5.4/3,可以说)可能允许缩小转换,而其他两个则明确不允许。即使我是一个顽固的赌徒,我也不会押上一分钱去打赌编译器会真正识别这种区别,并允许在一个案例中进行缩小转换,而在其他情况下则不允许(我觉得这有点令人惊讶,并且怀疑它是否意图允许)。


1

我在gcc 4.7.2上玩了一下,使用自定义类在构造函数中使用std::initializer_list。我尝试了所有这些场景以及更多。对于这三个语句,在该编译器上观察到的结果似乎没有任何区别。

编辑:这是我用于测试的确切代码:

#include <iostream>
#include <initializer_list>

class A {
public:
  A()                    { std::cout << "A::ctr\n"; }
  A(const A&)            { std::cout << "A::ctr_copy\n"; }
  A(A&&)                 { std::cout << "A::ctr_move\n"; }
  A &operator=(const A&) { std::cout << "A::=_copy\n"; return *this; }
  A &operator=(A&&)      { std::cout << "A::=_move\n"; return *this; }
  ~A()                   { std::cout << "A::dstr\n"; }
};

class B {
  B(const B&)            { std::cout << "B::ctr_copy\n"; }
  B(B&&)                 { std::cout << "B::ctr_move\n"; }
  B &operator=(const B&) { std::cout << "B::=copy\n"; return *this; }
  B &operator=(B&&)      { std::cout << "B::=move\n"; return *this; }
public:
  B(std::initializer_list<A> init) { std::cout << "B::ctr_ user\n"; }
  ~B()                             { std::cout << "B::dstr\n"; }
};

int main()
{
  B a1{ {}, {}, {} };
  B a2({ {}, {}, {} });
  B a3 = { {}, {}, {} };
  // B a4 = B{ {}, {}, {} }; // does not compile on gcc 4.7.2, gcc 4.8 and clang (top version)
  std::cout << "--------------------\n";
}

a1a2a3 在 gcc 4.7.2、gcc 4.8 和最新的 clang 上编译都很好。对于所有三种情况,我也没有看到在列表成员上执行的操作数量之间有任何可观察的结果。如果我将 B 的复制/移动构造函数设为私有/删除,则最后一种情况(不是来自问题)无法编译。


我认为第三个需要可用的复制构造函数或复制赋值运算符,但我不确定是哪一个。 - Mooing Duck
我也检查了那种情况 :-) 它没有问题。问题只出现在这个代码上:Foo c = Foo{ 2, 3, 5, 7};。顺便说一下,为了使其正常工作需要移动构造函数(如果未实现移动构造函数,则需要复制构造函数)。 - Mateusz Pusz
好的,那么原帖中的第三个示例需要存在移动/复制构造函数,但前两个示例则不需要。因此,它们至少在这方面是不同的。 - Mooing Duck
1
没有私有/删除的复制和移动构造函数,Foo c = {2,3,5,7}; 可以编译成功,但Foo c = Foo {2,3,5,7}; 不行。至少在我的gcc上是这样。所以目前,在我的编译器上,问题中的三种情况都是一样的。 - Mateusz Pusz
@MateuszPusz,我不知道你在说什么。gcc-4.5.1声称Foo c = { 2, 3, 5, 7};需要一个复制构造函数。 - Mooing Duck
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接