std::initializer_list

在 C++ 中考虑一个固定长度的整数数组:

int array[5];

若希望用一组值初始化该数组,可直接使用初始化器列表(initializer list)语法:

#include <iostream>

int main()
{
    int array[] { 5, 4, 3, 2, 1 }; // 初始化器列表
    for (auto i : array)
        std::cout << i << ' ';

    return 0;
}

输出:

5 4 3 2 1

动态分配的数组同样适用:

#include <iostream>

int main()
{
    auto* array{ new int[5]{ 5, 4, 3, 2, 1 } }; // 初始化器列表
    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';
    delete[] array;

    return 0;
}

在前一课程中,我们引入了容器类的概念,并给出了一个保存整数数组的 IntArray 类示例:

#include <cassert> // for assert()
#include <iostream>

class IntArray
{
private:
    int m_length{};
    int* m_data{};

public:
    IntArray() = default;

    IntArray(int length)
        : m_length{ length }
        , m_data{ new int[static_cast<std::size_t>(length)] {} }
    {
    }

    ~IntArray()
    {
        delete[] m_data;
        // 此处无需将 m_data 置空或将 m_length 置 0,因为对象将在该函数执行后立即销毁
    }

    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }

    int getLength() const { return m_length; }
};

int main()
{
    // 若尝试对容器类使用初始化器列表会如何?
    IntArray array { 5, 4, 3, 2, 1 }; // 本行无法通过编译
    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';

    return 0;
}

此代码无法编译,因为 IntArray 类没有能够处理初始化器列表的构造函数。于是只能逐个元素初始化:

int main()
{
    IntArray array(5);
    array[0] = 5;
    array[1] = 4;
    array[2] = 3;
    array[3] = 2;
    array[4] = 1;

    for (int count{ 0 }; count < 5; ++count)
        std::cout << array[count] << ' ';

    return 0;
}

显然,这并不理想。


使用 std::initializer_list 的类初始化

当编译器遇到初始化器列表时,会自动将其转换为 std::initializer_list 类型的对象。因此,若我们编写一个以 std::initializer_list 为参数的构造函数,便可通过初始化器列表来创建对象。

std::initializer_list 位于 <initializer_list> 头文件中。

关于 std::initializer_list 的几点须知:

  • std::arraystd::vector 类似,必须使用尖括号指明列表所含数据类型,除非立即初始化 std::initializer_list。因此几乎不会看到裸写的 std::initializer_list,而通常是 std::initializer_list<int>std::initializer_list<std::string> 等形式。
  • std::initializer_list 提供一个(命名不当的)size() 成员函数,返回列表元素个数,在需要知道列表长度时很有用。
  • std::initializer_list 常以值传递。与 std::string_view 类似,它是轻量级视图,复制时不会复制其中的元素。

下面用 std::initializer_list 更新 IntArray 类。

#include <algorithm> // for std::copy
#include <cassert>   // for assert()
#include <initializer_list> // for std::initializer_list
#include <iostream>

class IntArray
{
private:
    int m_length {};
    int* m_data {};

public:
    IntArray() = default;

    IntArray(int length)
        : m_length{ length }
        , m_data{ new int[static_cast<std::size_t>(length)] {} }
    {
    }

    // 允许通过列表初始化构造 IntArray
    IntArray(std::initializer_list<int> list)
        : IntArray(static_cast<int>(list.size())) // 委托构造函数完成初始数组设置
    {
        // 从列表初始化数组
        std::copy(list.begin(), list.end(), m_data);
    }

    ~IntArray()
    {
        delete[] m_data;
        // 此处无需将 m_data 置空或将 m_length 置 0,因为对象将在该函数执行后立即销毁
    }

    IntArray(const IntArray&) = delete;            // 避免浅拷贝
    IntArray& operator=(const IntArray&) = delete; // 避免浅拷贝

    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }

    int getLength() const { return m_length; }
};

int main()
{
    IntArray array{ 5, 4, 3, 2, 1 }; // 初始化器列表
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';

    return 0;
}

输出符合预期:

5 4 3 2 1

深入探讨

以下是接受 std::initializer_list<int> 的 IntArray 构造函数:

IntArray(std::initializer_list<int> list) // 允许列表初始化
    : IntArray(static_cast<int>(list.size())) // 委托构造函数
{
    // 从列表初始化数组
    std::copy(list.begin(), list.end(), m_data);
}
  • 第 1 行:如上所述,必须使用尖括号指明列表元素类型。此处因是 IntArray,期望列表元素为 int。注意不通过 const 引用传递;与 std::string_view 类似,std::initializer_list 本身很轻量,值传递通常比间接访问更廉价。
  • 第 2 行:通过委托构造函数完成内存分配。该构造函数需知数组长度,故传入 list.size()list.size() 返回 size_t(无符号),需强制转换为带符号的 int
  • 构造函数体负责将列表中的元素复制到 IntArray 内。最简单的方法是使用 <algorithm> 中的 std::copy()

访问 std::initializer_list 的元素

在某些场景下,你可能希望在把元素复制到内部数组之前访问 std::initializer_list 的每个元素(例如做值校验或修改)。

由于某些难以解释的原因,std::initializer_list 并未提供下标运算符(operator[])来访问元素。该缺陷已被多次向标准委员会提出,但始终未被采纳。

然而,有多种简单替代方案:

  • 使用范围 for 循环遍历列表元素。
  • 另一种方式是使用 begin() 成员函数获得迭代器。由于该迭代器为随机访问迭代器,可通过索引访问:
IntArray(std::initializer_list<int> list)
    : IntArray(static_cast<int>(list.size()))
{
    for (std::size_t count{}; count < list.size(); ++count)
    {
        m_data[count] = list.begin()[count];
    }
}

列表初始化优先匹配列表构造函数

非空初始化器列表总会优先匹配列表构造函数,而非其它潜在匹配的构造函数。举例:

IntArray a1(5);   // 使用 IntArray(int),分配长度为 5 的数组
IntArray a2{ 5 }; // 使用 IntArray(std::initializer_list<int>),分配长度为 1 的数组
  • a1 使用直接初始化(不会考虑列表构造函数),因此调用 IntArray(int),分配长度为 5 的数组。
  • a2 使用列表初始化(优先列表构造函数)。IntArray(int)IntArray(std::initializer_list<int>) 均可匹配,但因列表构造函数优先级更高,将调用 IntArray(std::initializer_list<int>),分配长度为 1 的数组(元素值为 5)。

因此,上述委托构造函数使用直接初始化:

IntArray(std::initializer_list<int> list)
    : IntArray(static_cast<int>(list.size())) // 使用直接初始化

确保委托到 IntArray(int)。若此处使用列表初始化,构造函数将尝试自我委托,导致编译错误。

std::vector 等同时具有列表构造函数与相似参数构造函数的容器类亦存在同样现象:

std::vector<int> array(5); // 调用 vector::vector(size_type),5 个值初始化为 0 的元素:0 0 0 0 0
std::vector<int> array{ 5 }; // 调用 vector::vector(initializer_list<int>),1 个元素:5

关键洞察

列表初始化会优先匹配列表构造函数,而非非列表构造函数。


最佳实践

在初始化具有列表构造函数的容器时:

  • 欲调用列表构造函数,使用花括号初始化(因初始值为元素值)。
  • 欲调用非列表构造函数,使用直接初始化(因初始值非元素值)。

向既有类添加列表构造函数存在风险

由于列表初始化优先匹配列表构造函数,若向原本无此构造函数的类添加列表构造函数,可能导致已有程序行为静默改变。

考虑以下程序:

#include <initializer_list> // for std::initializer_list
#include <iostream>

class Foo
{
public:
    Foo(int, int)
    {
        std::cout << "Foo(int, int)" << '\n';
    }
};

int main()
{
    Foo f1{ 1, 2 }; // 调用 Foo(int, int)
    return 0;
}

输出:

Foo(int, int)

现在给此类增加列表构造函数:

#include <initializer_list> // for std::initializer_list
#include <iostream>

class Foo
{
public:
    Foo(int, int)
    {
        std::cout << "Foo(int, int)" << '\n';
    }

    // 新增列表构造函数
    Foo(std::initializer_list<int>)
    {
        std::cout << "Foo(std::initializer_list<int>)" << '\n';
    }
};

int main()
{
    // 注意:以下语句未作任何改动
    Foo f1{ 1, 2 }; // 现在调用 Foo(std::initializer_list<int>)
    return 0;
}

尽管未改动程序其他部分,输出变为:

Foo(std::initializer_list<int>)

警告

向原本无列表构造函数的类添加列表构造函数可能破坏已有程序。


使用 std::initializer_list 的类赋值

你也可通过重载接受 std::initializer_list 参数的赋值运算符,使用初始化器列表为类赋予新值。其做法与上述构造函数类似,我们将在下方测验答案中给出示例。


注意

若你实现了接受 std::initializer_list 的构造函数,应至少完成以下之一:

  • 提供重载的列表赋值运算符
  • 提供正确的深拷贝赋值运算符
  • 删除拷贝赋值运算符

原因如下:考虑以下类(未实现上述任一措施)及列表赋值语句:

#include <algorithm> // for std::copy()
#include <cassert>   // for assert()
#include <initializer_list> // for std::initializer_list
#include <iostream>

class IntArray
{
private:
    int m_length{};
    int* m_data{};

public:
    IntArray() = default;

    IntArray(int length)
        : m_length{ length }
        , m_data{ new int[static_cast<std::size_t>(length)] {} }
    {
    }

    IntArray(std::initializer_list<int> list)
        : IntArray(static_cast<int>(list.size()))
    {
        std::copy(list.begin(), list.end(), m_data);
    }

    ~IntArray()
    {
        delete[] m_data;
    }

    // IntArray(const IntArray&) = delete;
    // IntArray& operator=(const IntArray&) = delete;

    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }

    int getLength() const { return m_length; }
};

int main()
{
    IntArray array{};
    array = { 1, 3, 5, 7, 9, 11 }; // 列表赋值语句

    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' '; // 未定义行为

    return 0;
}

首先,编译器会注意到不存在接受 std::initializer_list 的赋值函数。随后它会寻找其他可用赋值函数,并发现隐式生成的拷贝赋值运算符。然而,此函数仅当能将初始化器列表转换为 IntArray 时方可使用。由于 { 1, 3, 5, 7, 9, 11 }std::initializer_list,编译器将使用列表构造函数将其转换为临时 IntArray,然后调用隐式赋值运算符,将临时 IntArray 浅拷贝到 array 对象。

此时,临时 IntArray 的 m_dataarray->m_data 指向同一地址(因浅拷贝)。后续情形可想而知。

赋值语句结束时,临时 IntArray 被销毁,调用析构函数并删除其 m_data,导致 array->m_data 变为悬空指针。任何对 array->m_data 的使用(包括 array 离开作用域时析构函数再次删除 m_data)将导致未定义行为。


最佳实践

若你提供了列表构造,最好也提供列表赋值。


总结

实现接受 std::initializer_list 参数的构造函数,可让我们在自定义类中使用列表初始化。我们亦可利用 std::initializer_list 实现需要初始化器列表的其他函数(如赋值运算符)。


测验时间

问题 1

使用上述 IntArray 类,实现一个接受初始化器列表的重载赋值运算符。

以下代码应能正常运行:

int main()
{
    IntArray array{ 5, 4, 3, 2, 1 }; // 初始化器列表
    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';

    std::cout << '\n';

    array = { 1, 3, 5, 7, 9, 11 };

    for (int count{ 0 }; count < array.getLength(); ++count)
        std::cout << array[count] << ' ';

    std::cout << '\n';

    return 0;
}

应输出:

5 4 3 2 1
1 3 5 7 9 11

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"