以下三个初始化与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
只是一个占位符,但我对一般答案感兴趣。
答案 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";
}
a1
,a2
和a3
在gcc 4.7.2,gcc 4.8和最新的clang上编译得很好。对于所有3个案例,我也没有看到列表成员的操作次数之间有任何可观察到的结果。如果我将B
复制/移动构造函数设为私有/删除,则最后一种情况(不是问题)不会编译。