浅谈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

参考资源

相关推荐
Cx330❀几秒前
深入浅出 Qt 绘图机制:从 QPainter 到四大图像类解析
开发语言·qt·ui·性能优化·图形渲染
信可维12 分钟前
AI 读 Word 表格总读错?先查合并单元格和表头这 3 处
开发语言·人工智能·c#·word·软件工程
Code_Solitude34 分钟前
传智杯备赛代码整理(前4题)
数据结构·算法
HEJOO939 分钟前
Java 并发运行中的安全问题详解
java·开发语言
星夜离尘561 小时前
MATLAB阶跃响应怎么量:别把最后一个采样点当成稳态
开发语言·matlab
Jasmine_llq1 小时前
《P8816 [CSP-J 2022] 上升点列》
数据结构·算法·贪心·排序·二维动态规划 dp·类背包状态转移
Android小码家1 小时前
Vehicle 实战 自定义属性+测试demo
java·c++·framework·vhal·vehicle
hetao17338371 小时前
2026-10-19 hetao1733837 的刷题记录
c++·算法
夏虫不可与之言冰1 小时前
第三课 点亮 LED 与定时器非阻塞闪烁
c语言·c++·vscode·单片机·嵌入式硬件·物联网
云水初1 小时前
【agent篇】RAG Agent避坑指南(附高级RAG Agent汇总)
开发语言·笔记·python·学习·langchain·agent·rag