用双花括号初始化向量:std::string vs int

Vector initialization with double curly braces: std::string vs int

在这个问题的回答中:

表明

vector<string> v = {{"a", "b"}};

将使用 initializer_list 一个元素 调用 std::vector 构造函数。因此向量中的第一个(也是唯一一个)元素将从 {"a", "b"} 构造。这会导致未定义的行为,但这超出了这里的重点。

我发现的是

std::vector<int> v = {{2, 3}};

将使用 initializer_list 两个元素 .

调用 std::vector 构造函数

造成这种行为差异的原因是什么?

行为上的差异是由于默认参数造成的。 std::vector 有这个客户:

vector( std::initializer_list<T> init, 
        const Allocator& alloc = Allocator() );

注意第二个参数。 {2, 3} 推导为 std::initializer_list<int>(不需要初始化器的转换)并且分配器是默认的。

class类型列表初始化的规则基本上是:首先,只考虑std::initializer_list个构造函数进行重载决议,然后,如果需要,对所有构造函数进行重载决议(这是[over.match.list]).

当从初始化列表中初始化一个 std::initializer_list<E> 时,就好像我们从初始化列表中的 N 个元素中具体化了一个 const E[N](来自 [dcl.init.list]/5).

对于 vector<string> v = {{"a", "b"}}; 我们首先尝试 initializer_list<string> 构造函数,这将涉及尝试初始化 1 const string 的数组,其中 string{"a", "b"}。这是 可行的 因为 string 的迭代器对构造函数,所以我们最终得到一个包含一个字符串的向量(这是 UB 因为我们违反了该字符串的先决条件构造函数)。这是简单的情况。


对于 vector<int> v = {{2, 3}};,我们首先尝试 initializer_list<int> 构造函数,这将涉及尝试初始化一个包含 1 const int 的数组,其中一个 int{2, 3}。这是不可行

那么,我们重新考虑所有 vector 构造函数的重载决策。现在,我们得到了两个可行的构造函数:

  • vector(vector&& ),因为当我们在那里递归初始化参数时,初始化列表将是 {2, 3} - 我们将尝试用它来初始化 2 const int 的数组,这是可行的。
  • vector(std::initializer_list<int> ),再次。这次不是来自正常的列表初始化世界,而是直接从相同的 {2, 3} 初始化列表中直接初始化 initializer_list,出于同样的原因,这是可行的。

要选择哪个构造函数,我们必须进入 [over.ics.list], where the vector(vector&& ) constructor is a user-defined conversion sequence but the vector(initializer_list<int> ) constructor is identity,所以它是首选。


为了完整起见,vector(vector const&) 也是可行的,但出于其他原因,我们更喜欢移动构造函数而不是复制构造函数。