为何std::initializer_list在std::vector构造函数中的表现不符合预期?
std::vector{std::vector<int>{}}被推断为std::vector<int>而非std::vector<std::vector<int>>? 先看你的代码:
#include <vector> int main() { auto v = std::vector{std::vector<int>{}}; return v.front().empty(); // error: 'int' has no member 'empty' }
你预期v是std::vector<std::vector<int>>,但编译器推断它为std::vector<int>,这看起来和Scott Meyers在《Effective Modern C++》里的观点冲突——他说有std::initializer_list构造函数的类,使用大括号初始化时会优先选择这个重载。
问题出在哪里?
Scott的观点没错,但它有一个关键前提:你使用的是括号内的初始化器列表(braced-init-list),而不是单个对象。我们来拆解两种情况:
1. 当你使用嵌套大括号(真正的初始化器列表)
如果你写成:
auto v = std::vector{{std::vector<int>{}}};
此时外层大括号里是一个braced-init-list(包含一个std::vector<int>对象),编译器会优先匹配std::vector的initializer_list构造函数,正确推断T为std::vector<int>,v就会是你预期的std::vector<std::vector<int>>类型。
2. 你的代码中的情况:单个对象作为初始化器
你写的std::vector{std::vector<int>{}}里,外层大括号的内容是单个std::vector<int>对象,不是初始化器列表。这时候Scott的优先级规则不适用,编译器会按照类模板参数推断(CTAD)的常规逻辑来寻找匹配的构造函数:
- 如果尝试推断
T = std::vector<int>:我们需要构造std::vector<std::vector<int>>,但它的构造函数里没有能直接接受单个std::vector<int>对象的重载(initializer_list构造需要初始化器列表,移动构造需要同类型的右值,输入迭代器构造需要两个参数),所以这个推断不成立。 - 如果尝试推断
T = int:我们需要构造std::vector<int>,它的移动构造函数正好接受std::vector<int>&&类型的参数,而std::vector<int>{}正是一个std::vector<int>的右值,完全匹配!
所以编译器会选择这个可行的推断,将v推断为std::vector<int>,自然v.front()是int类型,调用empty()就会报错。
总结
你误解了Scott观点的适用场景:它只针对初始化器是braced-init-list(即{元素1, 元素2,...})的情况。当你用单个对象作为大括号初始化的内容时,编译器会回到常规的重载解析和模板参数推断逻辑,优先选择有匹配构造函数的推断结果。
内容的提问来源于stack exchange,提问作者xmllmx




