[英]std::initializer_list variations
使用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
只是一个占位符,但我对一般答案很感兴趣。
在上面的例子中,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,7 {2, 3, 5, 7}
是一个braced-init-list,其中每个元素都具有相同的类型。 如果你有{2, 3, 5.3, 7.9}
,那么就不会检查initializer_list
构造函数。
T c = { 2, 3, 5, 7};
这将表现得像a
,除了它将做什么样的转换。 由于这是copy-list-initialization,它将尝试调用initializer_list构造函数。 如果没有这样的构造函数,它将尝试调用一个4参数构造函数,但它只允许将其for参数隐式转换为类型参数。
这是唯一的区别。 它不需要复制/移动构造函数或任何东西(规范只提到3个位置的复制列表初始化。当复制/移动构造不可用时,它们都不禁止它)。 除了它允许在其参数上进行的转换之外,它几乎完全等同于a
。
这就是为什么它通常被称为“统一初始化”:因为它在任何地方的工作方式几乎相同。
传统上(C ++ 98/03),初始化如T x(T());
调用直接初始化,并初始化如T x = T();
调用复制初始化。 当您使用复制初始化时,复制ctor必须存在,即使它可能不使用(即通常不使用)。
初始化程序列出了一种变化。 查看§8.5/ 14和§8.5/ 15表明直接初始化和复制初始化这两个术语仍然适用 - 但是看看§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
的构造函数重载std::initializer_list<T>
。 从任何实际的角度来看,三者之间没有区别。 所有三个都将调用vector::vector(std::initializer_list<T>
,没有发生任何副本或其他转换(甚至不是那些可能被删除并且仅在理论上发生的)。
我认为,只是略有不同的值,有(或至少可能)一个小的差异。 禁止缩小转换的禁令在§8.5.4/ 3中,所以你的第二个例子(不经过§8.5.4/ 3,可以这么说)应该允许缩小转换,而其他两个显然不允许。 然而,即使我是一个狡猾的赌徒,我也不会打赌编译器实际上认识到这种区别,并允许在一种情况下缩小转换而不是其他情况(我发现它有点令人惊讶,而是怀疑它是意图允许)。
让我们从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
参数,将使用列表初始化,因为这不是使用parens的直接初始化(因此参数初始化等效于T ref t = { 1 }
)。 前两个函数是完全匹配。 但是,标准说在这种情况下,当一个函数转换为std::initializer_list<T>
而另一个函数不转换时,则前一个函数获胜。 因此,在这种情况下,将使用第二个ctor
。 请注意,在这种情况下,我们不会使用第一个初始化列表ctors进行两阶段重载解析 - 只有列表初始化才会这样做 。
对于前两个,我们将使用列表初始化,它将执行与上下文相关的事情。 如果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>
。
我在gcc 4.7.2上玩了一个自定义类,在构造std::initializer_list
中使用了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
复制/移动构造函数设为私有/删除,则最后一种情况(不是问题)不会编译。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.