Skip to the content.

cpp std::initializer_list

std::initializer_list<T> 解决的核心问题是:让函数和构造函数能够用一个参数,接收 {...} 中列出的、数量不固定的一组元素。 最典型的用途就是让 vector、set 以及你自己定义的类,也能像数组一样方便地初始化。 stroustrup.com

1. 原来有什么不方便?

数组很早就支持这样的写法:

int a[] = {10, 20, 30};

但是在 C++11 之前,不能直接这样初始化 vector:

std::vector<int> v = {10, 20, 30};  // C++11 之前不支持

往往需要先准备数组,再利用迭代器区间构造 vector:

int a[] = {10, 20, 30};
std::vector<int> v(a, a + 3);

同样是“用这几个元素创建一个集合”,数组可以直接写,类类型却需要绕一下。 initializer_list 配合语言层面的列表初始化规则,补上了这个缺口。stroustrup.com

现在可以直接写:

std::vector<int> v{10, 20, 30};

vector 提供了这样的构造函数,省略分配器等细节后是:

vector(std::initializer_list<T> values);

所以,这里不是调用“接收三个 int 参数的构造函数”,而是调用“接收一个列表参数的构造函数”。 Eel

2. 自己的类和普通函数,也能使用这个能力

这不是 vector 独享的特殊语法。你可以给自己的类添加同样的接口:

#include <initializer_list>
#include <vector>

class Numbers {
    std::vector<int> data_;

public:
    Numbers(std::initializer_list<int> values)
        : data_(values)
    {}
};

Numbers a{1, 2};
Numbers b{1, 2, 3, 4, 5};

无论列出两个还是五个元素,调用的都是同一个构造函数,不需要针对每种元素数量写一个重载。stroustrup.com

虽然名字里有 initializer,它也可以用于普通函数:

#include <initializer_list>

int sum(std::initializer_list<int> values)
{
    int result = 0;
    for (int value : values) {
        result += value;
    }
    return result;
}

// 调用示例:
int a = sum({1, 2, 3});        // 6
int b = sum({10, 20, 30, 40}); // 100
int c = sum({});              // 0

注意,这里的调用是 sum({1, 2, 3}),不是 sum(1, 2, 3):传入的是一个列表参数,而不是三个独立参数。 stroustrup.com

3. 它本质上是什么?不是一个负责存储数据的 vector

可以把 std::initializer_list<T> 理解为:指向一组只读元素的轻量视图。

概念上类似下面这样,具体布局不由标准强制规定:

template<class T>
struct InitializerListModel {
    const T* elements;
    std::size_t count;
};

实际的 initializer_list 提供 begin()、end() 和 size() 等接口。复制它只复制这个“视图”,不会复制底层元素,因此函数通常直接按值接收它。Eel

对于:

sum({10, 20, 30});

可以把执行过程理解成:

// 示意:由编译器准备底层数组
const int backing_array[] = {10, 20, 30};

// 再让一个 initializer_list<int> 指向它,传给 sum。

编译器在语言规则的支持下完成这个过程;并不是用户自己调用某个公开的“指针+长度”构造函数。Eel

因此要区分:

对象 作用
initializer_list<int> 临时访问传进来的一组只读元素,不拥有它们
vector<int> 保存并管理自己的元素,之后可以修改、增加、删除

在前面的 Numbers 中,data_(values) 会把列表里的元素复制进 vector 自己的存储空间,而不是让 vector 一直引用这份列表。Eel

4. {...} 不等于 std::initializer_list

这是一个很容易混淆的地方:

int x{10};                   // 初始化一个 int

int a[]{1, 2, 3};            // 数组的聚合初始化

struct Point {
    int x;
    int y;
};
Point p{1, 2};               // 结构体的聚合初始化

std::vector<int> v{1, 2, 3}; // 调用 initializer_list 构造函数

这些写法都用了花括号,但只有最后一个例子涉及 vector 的 std::initializer_list 构造函数。

{...} 是语言语法;std::initializer_list<T> 是用于接收其中一组元素的标准库类型,二者不是同一个概念。 Eel

5. 两个必须知道的注意事项

花括号可能改变构造函数的选择

std::vector<int> a(3, 100); // 三个元素:100, 100, 100
std::vector<int> b{3, 100}; // 两个元素:3, 100

对于 vector 这样的类,使用非空花括号列表初始化时,会先尝试匹配 initializer_list 构造函数。因此,不能简单地认为 () 和 {} 可以随便互换。 Eel

不要把接收到的列表当成长期存储

下面这种写法存在生命周期问题:

class Bad {
    std::initializer_list<int> data_;

public:
    Bad(std::initializer_list<int> values)
        : data_(values)  // 只复制视图,不复制元素
    {}
};

Bad b{1, 2, 3};
// 这个分号之后,底层临时数组的生命周期已经结束。
// b.data_ 还在,但它引用的元素已经失效。

需要长期保存时,应像前面的 Numbers 一样,把元素复制进 vector 等拥有存储的对象。另一方面,直接声明局部列表变量是有效的:

std::initializer_list<int> values = {1, 2, 3};
// 底层数组的生命周期会延长到 values 的生命周期结束。

关键是:从 {...} 直接初始化列表可以触发生命周期延长,但再复制这个列表不会继续延长底层数组的生命周期。 Eel


可以把它记成一句话:

std::initializer_list 是“接收一组花括号元素”的接口工具,不是“长期保存一组元素”的容器。 Eel

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。