双大括号初始化向量:std::string与int的区别

6
在回答这个问题时:使用双花括号初始化vector<string>,展示了以下内容:
vector<string> v = {{"a", "b"}};

将使用一个仅包含一个元素初始化列表来调用std::vector构造函数,因此向量中的第一个(也是唯一的)元素将从{"a","b"}构造。这会导致未定义的行为,但这已经不是重点。

我发现的是:

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

将使用包含两个元素initializer_list调用std::vector构造函数。

为什么会出现这种行为差异呢?


它后面跟着对向量复制构造函数的调用吗? - StoryTeller - Unslander Monica
就我使用调试器所看到的,没有。 - bolov
嗯,有趣。 - StoryTeller - Unslander Monica
好的。经过一番仔细检查,我有了你的答案 :) 很棒的跟进。 - StoryTeller - Unslander Monica
2个回答

5
类类型的列表初始化规则基本上是:首先,只考虑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> ),再次使用直接初始化initializer_list的方式,从同样的{2, 3}初始化列表中进行初始化,出于同样的原因,这是可行的。
为了选择构造函数,我们需要进入[over.ics.list],其中vector(vector&& )构造函数是一个user-defined conversion sequence,但vector(initializer_list<int> )构造函数是identity,所以它更受欢迎。
完整性起见,vector(vector const&)也可行,但出于其他原因,我们更喜欢移动构造函数而不是复制构造函数。


2
行为上的差异是由于默认参数造成的。 std::vector 有以下构造函数:
vector( std::initializer_list<T> init, 
        const Allocator& alloc = Allocator() );

请注意第二个参数。 {2, 3} 被推断为 std::initializer_list<int>(不需要转换初始化程序),分配器被默认设置。

嗯,我看到这似乎与第一个案例中的 T=std::string 和参数 char [2] 有关,而在第二个案例中,我们有 T=int 和完全相同类型 int 的参数,但我无法弄清楚为什么会导致这些特定的行为。太让人头疼了 :)) - bolov
@bolov - 原因是ICS排名和C++是一个非常庞大的野兽。 - StoryTeller - Unslander Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接