尝试理解C++ STL容器的初始化

5

我想要理解C++ STL容器的初始化。以下是我遇到的噩梦:

    vector<int> V0 ({ 10, 20 });    // ok - initialized with 2 elements
    vector<int> V1 = { 10, 20 };    // ok - initialized with 2 elements
    vector<int> V2 = {{ 10, 20 }};  // ok - initialized with 2 elements
    vector<int> V3 = {{ 10 }, 20 }; // ok - initialized with 2 elements
    vector<int> V4 { 10, 20 };      // ok - initialized with 2 elements
    vector<int> V5 {{ 10, 20 }};    // ok - initialized with 2 elements
    vector<int> V6 {{ 10 }, 20 };   // ok - initialized with 2 elements
    queue<int> Q0 ({ 10, 20 });     // ok - initialized with 2 elements
 // queue<int> Q1 = { 10, 20 };     // compile error
 // queue<int> Q2 = {{ 10, 20 }};   // compile error
 // queue<int> Q3 = {{ 10 }, 20 };  // compile error
 // queue<int> Q4 { 10, 20 };       // compile error
    queue<int> Q5 {{ 10, 20 }};     // ok - initialized with 2 elements
 // queue<int> Q6 {{ 10 }, 20 };    // compile error

我们正在谈论C++11。
我做了一些研究,以下是我的问题:
1. 我认为编译错误是因为queue的initializer_list构造函数缺失。请参阅vector:http://www.cplusplus.com/reference/vector/vector/vector/和queue:http://www.cplusplus.com/reference/queue/queue/queue/,我的理解正确吗?
2. 现在对于从V0到V6的所有向量,我理解V0、V1、V4。有人可以帮我理解V2、V3、V5和V6吗?
3. 我不太理解Q0或Q5。有人可以帮助我吗?
我还在阅读Mike Lui的文章:“Initialization in C++ is Seriously Bonkers”。我想与大家分享,但是否有快速帮助我理解这个噩梦的方法? :-)

我可以理解 _nightmare_。;-) 一个反例:std::vector<int> V7(10, 20); 用10个元素初始化 V7Demo on coliru - Scheff's Cat
@Scheff:这是唯一的“噩梦”问题,而且它甚至不是OP提供的问题。其余所有问题都只是简单的、标准的列表初始化内容。 - Nicol Bolas
1个回答

8

这里没有任何“可怕”的事情。您只需要阅读您写的内容。更具体地说,您需要从外到内系统地按照规则进行排版。

vector<int> V0 ({ 10, 20 });

调用一个 vector 构造函数(这就是 () 所表示的),并将单个的大括号初始化列表作为参数传递给它。因此,它会选择一个只接受一个值的构造函数,但只有第一个参数可以由包含整数的两个元素的大括号初始化列表进行初始化的构造函数才能被选中。例如 vector<int> 包含的 initializer_list<int> 构造函数。
vector<int> V1 = { 10, 20 };

列表初始化(当您直接使用花括号初始值列表初始化一个对象时发生的情况)。在列表初始化规则下,所有只接受单个initializer_list参数的类型的构造函数被首先考虑。系统会尝试直接使用花括号初始值列表来初始化这些构造函数;如果它能够成功地使用其中一个候选构造函数,则调用该构造函数。
显然,您可以从整数的2元素花括号初始值列表中初始化一个initializer_list<int>。而且这是vector中唯一的initializer_list构造函数,因此它会被调用。
vector<int> V2 = {{ 10, 20 }};

仍然是列表初始化。同样,与花括号初始化列表中的值匹配的initializer_list构造函数被视为可用。但是,花括号初始化列表中的“value”本身就是另一个花括号初始化列表。int无法从具有两个元素的花括号初始化列表中初始化,因此initializer_list<int>不能通过{{10,20}}进行初始化。
由于不能使用任何initializer_list构造函数,在正常函数重载解析规则下会考虑所有构造函数。在这种情况下,外部花括号初始化列表的成员被视为类型的构造函数的参数。外部花括号初始化列表中只有一个值,因此仅考虑可以带一个参数调用的构造函数。
系统将尝试使用内部花括号初始化列表来初始化所有这类构造函数的第一个参数。并且存在一种构造函数,其参数可以通过整数的2个元素的花括号初始化列表进行初始化。也就是说,虽然initializer_list<int>不能通过{{10,20}}进行初始化,但它可以通过{10,20}进行初始化。
vector<int> V3 = {{ 10 }, 20 };

再次提到,列表初始化。我们首先尝试将完整的花括号初始化列表应用于类型的任何 initializer_list 构造函数。可以用 {{10}, 20} 这样的花括号初始化列表对 initializer_list<int> 进行初始化吗?可以。所以就是这样。

为什么会起作用呢?因为任何可复制/可移动的类型 T 都可以从一个包含该类型某个值的花括号初始化列表中进行初始化。也就是说,如果 T t = some_val; 起作用,那么 T t = {some_val}; 也会起作用(除非 T 具有接受 Tinitializer_list 构造函数,这显然是很奇怪的)。如果 T t = {some_val}; 起作用,那么 initializer_list<T> il = {{some_val}}; 也会起作用。

vector<int> V4 { 10, 20 };      // ok - initialized with 2 elements
vector<int> V5 {{ 10, 20 }};    // ok - initialized with 2 elements
vector<int> V6 {{ 10 }, 20 };   // ok - initialized with 2 elements

这些与1、2和3相同。列表初始化通常被称为"统一初始化",因为直接使用大括号初始化列表与使用=大括号初始化列表之间几乎没有区别。唯一出现区别的情况是选择了显式构造函数或者在大括号初始化列表中使用auto和单个值。


queueinitializer_list构造函数并不是"缺失"的。这是有意的,因为queue不是一个容器,而是一个容器适配器类型。它存储一个容器,并调整容器的接口以仅限于队列操作:推入(push)、弹出(pop)和查看(peek)。

所以,这些都不应该起作用。

queue<int> Q0 ({ 10, 20 });

这将使用常规的重载决议来调用 queue<int> 的构造函数,就像 V0 一样。唯一的区别是它选择的构造函数是采用队列容器类型的构造函数。由于您没有指定容器,它使用默认值:std::deque<int>,这可以从两个整数的花括号初始化列表构造。
queue<int> Q5 {{ 10, 20 }};

V2一样的情况。在queue中没有initializer_list构造函数,因此它的行为与Q0完全相同:使用重载分辨率来选择一个可以接受2个整数括号初始化列表作为参数的构造函数。


你能否举例详细解释一下答案,以便像我这样的新手能够理解? - TruthSeeker
1
@TruthSeeker:需要什么样的例子?“新手”一开始不应该深入研究这些东西。如果你想初始化一个容器,你应该只使用V1并继续前进。 - Nicol Bolas
感谢@NicolBolas。非常感激。解释很好,尽管我仍然花了几个小时才理解它。我之所以问这个问题,是因为我发现现在C++语言变得越来越难理解了(我曾经为“知道C ++”而自豪)。 我甚至在想,如果值类型是pair<int,int>而不是纯粹的int,会怎样呢? :-) 这将变得非常混乱,即使对于一个老练的C++专家来说,也需要很长时间才能理解。 - Peter Lee

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