std :: initializer_list变种

时间:2012-11-19 17:31:42

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

以下三个初始化与std::initializer_list s?

之间有何区别?
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只是一个占位符,但我对一般答案感兴趣。

4 个答案:

答案 0 :(得分:2)

  

在上面的例子中,std :: vector只是一个占位符,我对一般答案很感兴趣。

你想要一个答案的“一般”吗?因为真正的含义取决于您初始化的类型以及它们具有的构造函数。

例如:

T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );

这些可能是两个不同的东西。或者他们可能没有。这取决于构造函数T。如果T的构造函数只有一个initializer_list<int>(或其他initializer_list<U>,其中U是一个整数类型),那么这两个构造函数都将调用该构造函数。< / p>

然而,如果没有那个,那么这两个会做不同的事情。第一个,将尝试调用一个构造函数,该构造函数接受可由整数文字生成的4个参数。第二个将尝试调用一个带有一个参数的构造函数,它将尝试使用{2, 3, 5, 7}进行初始化。这意味着它将遍历每个单参数构造函数,找出该参数的类型,并尝试使用R{2, 3, 5, 7}构造它。如果这些都不起作用,那么它将尝试将其作为{ {1}}。如果这不起作用,那就失败了。

initializer_list<int>构造函数始终具有优先级。

请注意,initializer_list构造函数仅在进行中,因为initializer_list是一个braced-init-list,其中每个元素都具有相同的类型。如果您有{2, 3, 5, 7},则不会检查{2, 3, 5.3, 7.9}构造函数。

initializer_list

这将表现得像T c = { 2, 3, 5, 7}; ,除了它将执行的转换类型。由于这是copy-list-initialization,它将尝试调用initializer_list构造函数。如果没有这样的构造函数可用,它将尝试调用一个4参数构造函数,但它只允许隐式转换将其参数转换为类型参数。

这是唯一的区别。它不需要复制/移动构造函数或任何东西(规范只提到3个位置的复制列表初始化。当复制/移动构造不可用时,它们都不禁止它)。它几乎完全等同于a,除了它允许在其参数上进行的转换。

这就是为什么它通常被称为“统一初始化”:因为它在任何地方的工作方式几乎相同。

答案 1 :(得分:2)

传统上(C ++ 98/03),像T x(T());这样的初始化调用了直接初始化,而像T x = T();这样的初始化调用了复制初始化。当您使用复制初始化时,复制ctor必须存在,即使它可能没有(即通常没有)使用。

初始化程序列出了一种变化。查看§8.5/ 14和§8.5/ 15表明术语 direct-initialization copy-initialization 仍然适用 - 但是看看§8.5/ 16,我们发现对于支撑初始化列表,这是一个没有区别的区别,至少对于您的第一个和第三个示例:

  

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

因此,第一个和第三个示例的实际初始化完全相同,并且都不需要复制ctor(或移动ctor)。在这两种情况下,我们都在处理§8.5.4/ 3中的第四个项目:

  

- 否则,如果T是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决策(13.3,13.3.1.7)选择最佳构造函数。如果转换任何参数需要缩小转换(见下文),则程序格式不正确。

...所以两者都使用std::vector的ctor,其中std::initializer_list<T>为其参数。

然而,正如上面引用中所述,它仅涉及“(非括号内的)braced-init-list”。对于你的第二个带有括号括号的braced-init-list的例子,我们得到了§8.5/ 16的第六个子弹的第一个子弹(geeze - 真的需要与某些人谈论为那些添加数字):

  

- 如果初始化是直接初始化,或者它是复制初始化,其中源类型的cv-nonqualified版本与目标类的类相同,或者是派生类,则构造函数被视为。列举了适用的构造函数(13.3.1.3),并通过重载解析(13.3)选择最佳构造函数。调用所选的构造函数来初始化对象,初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载决策不明确,则初始化是不正确的。

由于这使用了直接初始化的语法,并且括号内的表达式是一个braced-initializer-list,std::vector有一个带有初始化列表的ctor,这就是选择的重载。

底线:虽然通过标准的路线不同,但所有三个最终都使用std::vector的{​​{1}}构造函数重载。从任何实际的角度来看,三者之间没有区别。这三个人都会调用std::initializer_list<T>,不会发生任何副本或其他转换(甚至不是那些可能被删除并且仅在理论上真正发生的转换)。

我认为,如果值略有不同,则存在(或至少可能存在)一个小的差异。禁止缩小转换的禁令在§8.5.4/ 3中,所以你的第二个例子(不经过§8.5.4/ 3,可以这么说)应该允许缩小转换,而其他两个显然不允许。然而,即使我是一个狡猾的赌徒,我也不会打赌编译器实际上认识到这种区别,并允许在一种情况下缩小转换而不是其他情况(我发现它有点令人惊讶,而是怀疑它是打算允许)。

答案 2 :(得分: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 必须是那里的类类型,即使支持的init列表只有1个参数。在这种情况下发布C ++ 11之前,我很高兴we put clear rules


就第三个调用构造函数而言,让我们假设

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参数,将使用列表初始化,因为这不是使用parens的直接初始化(因此参数初始化等效于T ref t = { 1 })。前两个函数是完全匹配。但是,标准说在这种情况下,当一个函数转换为std::initializer_list<T>而另一个函数不转换时,则前一个函数获胜。因此,在这种情况下,将使用第二个ctor请注意,在这种情况下,我们不会仅使用第一个初始化列表ctors执行两阶段重载解析 - 只有列表初始化才能执行此操作


对于前两个,我们将使用list-initialization,它将执行与上下文相关的事情。如果T是一个数组,它将初始化一个数组。以这个例子为例

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

T t = { 1L };

在这种情况下,我们执行两阶段重载决策。我们首先只考虑初始化列表构造函数并查看是否匹配,作为参数我们采用整个支撑初始化列表。第二个ctor匹配,所以我们选择它。我们将忽略第一个构造函数。如果我们没有初始化列表ctor或者没有匹配,我们将获取所有ctors和初始化列表的元素

struct T {
  T(long);

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

T t = { 1L };

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

答案 3 :(得分:1)

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

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

#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上编译得很好。对于所有3个案例,我也没有看到列表成员的操作次数之间有任何可观察到的结果。如果我将B复制/移动构造函数设为私有/删除,则最后一种情况(不是问题)不会编译。