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

参考资源

相关推荐
Joolun商城源码_Java1 小时前
Java 商城技术栈怎么选:单体、微服务还是前后端分离多端?
java·开发语言·微服务
拒绝内耗。1 小时前
一个请求进入 Java 应用后,怎么找到我们写的方法?
java·开发语言
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】RS_Line创建之后发生了什么?从图形容器到屏幕渲染
开发语言·c++·qt·学习·系统架构
夕除1 小时前
redis--013
java·开发语言
object not found1 小时前
Nuxt4去掉body中默认的边距
开发语言·后端·rust
Java后端的Ai之路1 小时前
Git pull弹出vim编辑器完整排查指南
开发语言·人工智能·git·编辑器·vim
青少儿编程课堂3 小时前
威佐夫博弈(双堆取子游戏)解析
c++·python·算法·bfs·信息学竞赛
一个初入编程的小白5 小时前
数据结构:二叉树链式结构的实现
数据结构
2501_941875287 小时前
从配置中心到动态管理的互联网工程语法演进与多语言实践分享
开发语言·python