浅谈C++的列表初始化std::initializer_list

浅谈C++的列表初始化std::initializer_list

兄弟们有群了:https://github.com/orgs/Awesome-Embedded-Learning-Studio/discussions/9

这个仓库已经开源!现代化 C++(C++11/14/17/20)从基础到进阶的系统教程都在这里,力争做一条完备的现代 C++ 学习路径!欢迎各位大佬前来参观,喜欢的话点个⭐!

Github 一键直达: https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP

看看超酷的新网站:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/
想找其他伙伴的话在这里!https://github.com/orgs/Awesome-Embedded-Learning-Studio/discussions/9,加入我们!

开始的第一步------它是什么呢?编译器为 {...} 生成的只读视图

std::initializer_list 是 C++11 给"花括号列表初始化"配的标准库类型。咱们写 vector<int>{1, 2, 3} 或 f({1, 2, 3}) 时,编译器会在背后构造一个 std::initializer_list<int>,代表 {1, 2, 3} 这段序列。它本身是个极轻量的对象------大致就是一个指针加一个长度,和 span 一样属于"不拥有数据的视图"。

cpp 复制代码
std::initializer_list<int> il = {1, 2, 3};   // 编译器构造,指向底层 const int[3]
il.size();        // 3
il.begin();       // 指向首元素
il.end();         // 尾后

几条性质捏在一起,决定了它的全部行为。它不拥有 元素,元素住在编译器生成的一段底层 const 数组里。元素是 const 的,只读。拷贝它又是浅拷贝,只拷那个指针和长度,不动元素。它最出名的坑就埋在这里,咱们往下看就会撞上。

它轻在于它浅拷贝

initializer_list 的拷贝是浅的:拷贝一个 initializer_list 就是拷它内部那个指针(和长度),底层那段 const 数组纹丝不动。所以咱们拿 initializer_list 传参几乎零成本,和传指针差不多。

cpp 复制代码
void f(std::initializer_list<int> il);   // 按值传,其实是浅拷贝(指针 + size)
f({1, 2, 3, 4, 5});   // 不拷贝 5 个 int,只传一个视图

但"元素是 const"这一点要记住:initializer_list 里的元素是 const T,咱们拿不到非 const 的访问。这看起来无害,却在和移动语义结合时挖了个大坑------下一节专门说。

移动陷阱:{...} 里的元素,进容器时只能拷贝

这是 initializer_list 最经典的坑。咱们想把几个对象塞进 vector,顺手写了 vector<T>{a, b, c},以为现代 C++ 会高效地移动它们------结果它们是拷贝进去的。

根因就在"元素是 const":initializer_list 的元素是 const T,而移动构造需要 T&&(非 const)。vector 从 initializer_list 构造时,得把每个 const 元素拷进自己的存储,const 拷不出 move,只能 copy。哪怕咱们在花括号里写 std::move,也只能让对象"移动进 initializer_list"(因为构造那一步接的是右值),可一旦进了 initializer_list 它就成了 const,再往 vector 里搬就只能拷贝了。

咱们量一下,看看到底拷了几次。用一个能统计拷贝 / 移动次数的类型:

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Counted {
    std::string s;
    inline static int copies = 0;
    inline static int moves = 0;
    Counted(std::string x) : s(std::move(x)) {}
    Counted(const Counted& o) : s(o.s) { ++copies; }
    Counted(Counted&& o) noexcept : s(std::move(o.s)) { ++moves; }
};

int main()
{
    // 场景 1:左值构造 initializer_list → vector
    {
        Counted a{"a"}, b{"b"}, c{"c"};
        Counted::copies = 0;
        Counted::moves = 0;
        std::vector<Counted> v{a, b, c};
        std::cout << "vector{a,b,c}        : copies=" << Counted::copies
                  << " moves=" << Counted::moves << "\n";
    }
    // 场景 2:move 进 initializer_list → vector(陷阱:进 vector 那步还是拷贝)
    {
        Counted a{"a"}, b{"b"}, c{"c"};
        Counted::copies = 0;
        Counted::moves = 0;
        std::vector<Counted> v{std::move(a), std::move(b), std::move(c)};
        std::cout << "vector{move(a),...}  : copies=" << Counted::copies
                  << " moves=" << Counted::moves << "\n";
    }
    // 场景 3:不用 initializer_list,push_back(move) → 全移动
    {
        Counted a{"a"}, b{"b"}, c{"c"};
        Counted::copies = 0;
        Counted::moves = 0;
        std::vector<Counted> v;
        v.reserve(3);
        v.push_back(std::move(a));
        v.push_back(std::move(b));
        v.push_back(std::move(c));
        std::cout << "push_back(move)      : copies=" << Counted::copies
                  << " moves=" << Counted::moves << "\n";
    }
    return 0;
}
bash 复制代码
g++ -std=c++17 -O2 -o /tmp/init_list_test /tmp/init_list_test.cpp && /tmp/init_list_test
text 复制代码
vector{a,b,c}        : copies=6 moves=0
vector{move(a),...}  : copies=3 moves=3
push_back(move)      : copies=0 moves=3

咱们把三个场景对比着看。第一种 vector{a, b, c}(左值):6 次拷贝、0 次移动------3 次拷贝构造 initializer_list 的元素,再 3 次拷贝进 vector。第二种 vector{std::move(a), ...} 是 3 次拷贝、3 次移动:std::move 让对象移动进了 initializer_list(省了 3 次拷贝),但进 vector 那一步还是 3 次拷贝,const 移不动。第三种 push_back(std::move(...)) 是 0 拷贝、3 移动:绕开 initializer_list,直接 move 进 vector,零拷贝。

所以这个坑咱们得记住:把若干对象塞进容器,vector{move(a), ...} 仍会拷贝进 vector,只有 push_back(move) 才零拷贝。当 T 是重型类型(大 string、大 vector),这个差距是实打实的拷贝开销。

花括号优先:为什么 {...} 总爱匹配 initializer_list 构造

initializer_list 还有个咱们得提防的"重载偏好":只要一个类的构造函数有 initializer_list 版本,花括号初始化就会优先选它,哪怕别的构造函数看起来更"合身"。最经典的翻车现场是 vector<int>:

cpp 复制代码
std::vector<int> v1(10, 0);    // 圆括号:10 个 0(count + value 构造)
std::vector<int> v2{10, 0};    // 花括号:两个元素 10 和 0(initializer_list 构造!)

咱们对比着看:v1 是 10 个 0,v2 是 {10, 0} 两个元素------同一段意图,圆括号和花括号给出了完全不同的结果,就因为花括号优先匹配了 initializer_list<int> 构造。这不是 bug,是规则:花括号初始化在有 initializer_list 构造时优先它。所以构造容器时,(count, value) 和 {a, b} 别混用,意图不同就用不同括号。

PS!TAMCPP的运行实例在这里,戳一下即可:

https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/vol3-standard-library/containers/11-initializer-lists

参考资源

相关推荐
hnjzsyjyj24 分钟前
洛谷 P1339:[USACO09OCT] Heat Wave G ← Bellman-Ford 算法
数据结构·bellman-ford 算法
lengxuenong39 分钟前
枚举例题分析
c++·算法·青少年编程
“AI国潮设计-小江”1 小时前
【SDXL实战】用AI生成“财神爷蛋糕×英歌舞人物”潮汕国潮甜品IP,附Prompt与批量生成思路
开发语言·人工智能·python·aigc
繁华的地方不一定留下你的脚印1 小时前
C++线程如何安全停止与唤醒:std::jthread、stop_token与condition_variable
开发语言·c++
caoerzhong1 小时前
跨境海外仓怎么管:JeeWMS 开源 Java 仓库管理系统打通头程、海外仓与尾程
java·开发语言·开源
码云数智-大飞1 小时前
新手写 Python 代码,如何规范命名、减少 Bug
开发语言·python·php
福兮说1 小时前
JS 正则的六个坑:带 g 的 test() 一真一假、空匹配死循环、replace 里的 $
开发语言·前端·javascript·正则表达式
码艺-Alimjan1 小时前
Web 网站打包桌面应用的另一种方式,超级简单(C# exe 33Kb)
开发语言·前端·c#
沫璃染墨1 小时前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
shada2 小时前
CMake指定 MSVC 工具集与 Windows SDK
c++·windows