5

下面三个用s初始化有什么区别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只是一个占位符,但我对一般答案感兴趣。

4

4 回答 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构造函数始终具有优先权。

请注意,initializer_list构造函数仅在起作用,因为{2, 3, 5, 7}它是一个花括号初始化列表,其中每个元素都具有相同的类型。如果你有{2, 3, 5.3, 7.9},那么它不会检查initializer_list构造函数。

T c = { 2, 3, 5, 7};

这将表现得像a,除了它会做什么样的转换。由于这是复制列表初始化,它将尝试调用一个 initializer_list 构造函数。如果没有这样的构造函数可用,它将尝试调用 4 参数构造函数,但它只允许将其 for 参数隐式转换为类型参数。

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

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

于 2012-11-19T19:08:51.513 回答
2

传统上(C++98/03),初始化像T x(T());调用直接初始化,初始化像T x = T();调用复制初始化。当您使用复制初始化时,需要存在可用的复制ctor,即使它可能未使用(即通常未使用)。

初始化程序列出了一种更改。查看 §8.5/14 和 §8.5/15 表明术语直接初始化复制初始化仍然适用——但查看 §8.5/16,我们发现对于带括号的初始化列表,这是一个没有区别的区别,至少对于您的第一个和第三个示例:

— 如果初始值设定项是(无括号的)大括号初始化列表,则对象或引用是列表初始化的(8.5.4)。

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

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

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

然而,如上面的引用所述,这只处理“(非括号)括号初始化列表”。对于带有括号括起来的初始化列表的第二个示例,我们进入第 8.5/16 节的第六个项目符号的第一个子项目符号(geeze - 真的需要与某人讨论为这些项目添加数字):

— 如果初始化是直接初始化,或者如果是复制初始化,其中源类型的 cv 非限定版本与目标类相同或派生类,则考虑构造函数。枚举了适用的构造函数(13.3.1.3),并通过重载决议(13.3)选择最佳构造函数。调用如此选择的构造函数来初始化对象,使用初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载决议不明确,则初始化格式错误。

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

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

但是,我相信值略有不同,但存在(或至少可能存在)一个微小的差异。§8.5.4/3 禁止缩小转换,因此您的第二个示例(可以说,它没有通过 §8.5.4/3)可能应该允许缩小转换,而其他两个显然没有。但是,即使我是个老赌徒,我也不会在编译器真正认识到这种区别并允许在一种情况下而不是其他情况下进行缩小转换上打赌(我觉得这有点令人惊讶,并且相当怀疑它是打算被允许)。

于 2012-11-19T19:10:45.690 回答
2

让我们从std::vector. 并调用它T

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

前两种形式是列表初始化(它们之间唯一的区别是 ifT是一个类,因为第二种explicit构造函数是禁止调用的。如果调用了一个,程序就变成了病态的)。最后一种形式只是普通的直接初始化,正如我们在 C++03 中所知道的那样:

T t(arg);

出现{a, b, c}as arg意味着构造函数调用的参数是一个大括号初始值设定项列表。第三种形式没有列表初始化所具有的特殊处理。T 那里必须是类类型,即使花括号初始化列表只有 1 个参数。我很高兴我们在这种情况下发布 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将使用第二个。请注意,在这种情况下,我们不会使用第一个初始化列表 ctor 进行两阶段重载解析 - 只有列表初始化会这样做


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

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

T t = { 1L };

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

struct T {
  T(long);

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

T t = { 1L };

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

于 2012-11-21T20:41:41.047 回答
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如果我将复制/移动构造函数设为私有/删除,则最后一种情况(不是来自问题)不会编译。

于 2012-11-19T17:51:48.417 回答