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
只是一个占位符,但我对一个通用答案感兴趣。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
只是一个占位符,但我对一个通用答案感兴趣。T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );
T
的情况。如果 T
有一个接受单个 initializer_list<int>
(或其他一些 initializer_list<U>
,其中 U
是整数类型)的构造函数,则这两个都将调用该构造函数。{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参数隐式转换为类型参数。a
完全相同,除了它允许在其参数上进行的转换类型。让我们抽象化 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 DantonT x(T())
这样的初始化调用直接初始化,而像 T x = T()
这样的初始化则调用复制初始化。当使用复制初始化时,即使可能不需要,复制构造函数也必须存在并可用。std::initializer_list
参数的 std::vector
的构造函数。std::vector
具有接受初始化程序列表的构造函数,因此选择该重载。
总之,尽管通过标准的路线不同,但这三种方式最终都使用了std::vector
的构造函数重载来创建std::initializer_list<T>
。从任何实用的角度来看,这三种方式没有区别。所有三种方式都会调用vector::vector(std::initializer_list<T>
,而且没有复制或其他转换发生(即使是理论上可能被省略的转换也没有发生)。
然而,我认为在稍微不同的值下,可能会有一些小差异。缩小转换的禁止在§8.5.4/3中规定,因此第二个例子(它没有经过§8.5.4/3,可以说)可能允许缩小转换,而其他两个则明确不允许。即使我是一个顽固的赌徒,我也不会押上一分钱去打赌编译器会真正识别这种区别,并允许在一个案例中进行缩小转换,而在其他情况下则不允许(我觉得这有点令人惊讶,并且怀疑它是否意图允许)。
我在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";
}
a1
、a2
和 a3
在 gcc 4.7.2、gcc 4.8 和最新的 clang 上编译都很好。对于所有三种情况,我也没有看到在列表成员上执行的操作数量之间有任何可观察的结果。如果我将 B
的复制/移动构造函数设为私有/删除,则最后一种情况(不是来自问题)无法编译。
Foo c = Foo{ 2, 3, 5, 7};
。顺便说一下,为了使其正常工作需要移动构造函数(如果未实现移动构造函数,则需要复制构造函数)。 - Mateusz PuszFoo c = {2,3,5,7};
可以编译成功,但Foo c = Foo {2,3,5,7};
不行。至少在我的gcc上是这样。所以目前,在我的编译器上,问题中的三种情况都是一样的。 - Mateusz Pusz
std::vector
还是关于初始化的一般性问题? - David Rodríguez - dribeasstd::vector<int> c = std::vector<int>{ 2, 3, 5, 7};
与问题中的不同,是否应该添加?我不确定。 - Mooing Duck