23-7 std::initializer_list

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

int array[5];

若需为该数组赋初始值,可直接通过初始化列表语法实现:

#include <iostream>

int main()
{
	int array[] { 5, 4, 3, 2, 1 }; // initializer list
	for (auto i : array)
		std::cout << i << ' ';

	return 0;
}

这将输出:

image

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

#include <iostream>

int main()
{
	auto* array{ new int[5]{ 5, 4, 3, 2, 1 } }; // initializer list
	for (int count{ 0 }; count < 5; ++count)
		std::cout << array[count] << ' ';
	delete[] array;

	return 0;
}

image

在上节课中,我们介绍了容器类的概念,并展示了一个名为 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;
        // we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
    }

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

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

int main()
{
	// What happens if we try to use an initializer list with this container class?
	IntArray array { 5, 4, 3, 2, 1 }; // this line doesn't compile
	for (int count{ 0 }; count < 5; ++count)
		std::cout << array[count] << ' ';

	return 0;
}

image

这段代码无法编译,因为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;
}

image

那不太好。


使用 std::initializer_list 进行类初始化

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

std::initializer_list 定义在 <initializer_list> 头文件中。

关于 std::initializer_list 有几点需知:与 std::array 或 std::vector 类似,除非立即初始化,否则必须使用尖括号指定列表的数据类型。因此几乎不会见到纯粹的 std::initializer_list。常见形式如 std::initializer_list 或 std::initializer_liststd::string

其次,std::initializer_list 提供了一个(命名不准确的)size() 函数,用于返回列表元素数量。当需要获取传入列表的长度时,此函数非常实用。

第三,std::initializer_list通常按值传递。它与std::string_view类似,本质上是一种视图。复制std::initializer_list时不会复制列表中的元素。

现在我们来更新IntArray类,添加一个接受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) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

	~IntArray()
	{
		delete[] m_data;
		// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
	}

	IntArray(const IntArray&) = delete; // to avoid shallow copies
	IntArray& operator=(const IntArray& list) = delete; // to avoid shallow copies

	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 }; // initializer list
	for (int count{ 0 }; count < array.getLength(); ++count)
		std::cout << array[count] << ' ';

	return 0;
}

这产生了预期的结果:

image

成功了!现在让我们深入探讨一下。

这是我们的 IntArray 构造函数,它接受一个 std::initializer_list

IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
	: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
{
	// Now initialize our array from the list
	std::copy(list.begin(), list.end(), m_data);
}

第1行:如上所述,我们必须使用尖括号来标明列表内部期望的元素类型。本例中,由于这是个整型数组(IntArray),因此列表应填充整数类型。请注意我们并未通过const引用传递列表。与std::string_view类似,std::initializer_list结构非常轻量级,其复制操作通常比间接引用更高效。

第2行:我们通过委托构造函数将IntArray的内存分配任务转交给另一个构造函数(以减少冗余代码)。该构造函数需要知道数组长度,因此我们传递list.size()——该值包含列表中的元素数量。需注意list.size()返回的是size_t类型(无符号整数),因此这里需要强制转换为有符号整数。

构造函数主体用于将列表元素复制到IntArray类中。最简便的方法是使用头文件中的std::copy()函数。


访问 std::initializer_list 的元素

在某些情况下,您可能需要在将元素复制到内部数组之前访问 std::initializer_list 的每个元素(例如进行值的合理性检查或修改这些值)。

出于某些难以解释的原因,std::initializer_list 并未提供通过下标操作符(operator[])访问列表元素的功能。该缺陷虽多次向标准委员会反馈,却始终未获解决。

不过存在若干简易变通方案:

  • 可使用基于范围的 for 循环遍历列表元素。
  • 另一种方法是调用 begin() 成员函数获取指向 std::initializer_list 的迭代器。由于该迭代器属于随机访问迭代器,因此可通过索引访问:
IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
	: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
{
	// Now initialize our array from the list
	for (std::size_t count{}; count < list.size(); ++count)
	{
		m_data[count] = list.begin()[count];
	}
}

列表初始化优先使用列表构造函数而非非列表构造函数

非空初始化列表始终优先匹配初始化列表构造函数,而非其他可能匹配的构造函数。请看以下示例:

IntArray a1(5);   // uses IntArray(int), allocates an array of size 5
IntArray a2{ 5 }; // uses IntArray<std::initializer_list<int>, allocates array of size 1

a1 情况采用直接初始化(不考虑列表构造函数),因此该定义将调用 IntArray(int),分配一个大小为 5 的数组。

a2 情况采用列表初始化(优先使用列表构造函数)。此处 IntArray(int) 和 IntArray(std::initializer_list) 均可能匹配,但因列表构造函数优先,故将调用 IntArray(std::initializer_list),分配大小为 1 的数组(该元素值为 5)。

这正是上述委托构造函数在委托时采用直接初始化的原因:

IntArray(std::initializer_list<int> list)
	: IntArray(static_cast<int>(list.size())) // uses direct init

这确保了我们委托给 IntArray(int) 版本。如果我们改用列表初始化进行委托,构造函数将试图委托给自己,从而导致编译错误。

对于同时具有列表构造函数和类似类型参数构造函数的 std::vector 及其他容器类,也会发生同样的情况。

std::vector<int> array(5); // Calls std::vector::vector(std::vector::size_type), 5 value-initialized elements: 0 0 0 0 0
std::vector<int> array{ 5 }; // Calls std::vector::vector(std::initializer_list<int>), 1 element: 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 }; // calls Foo(int, int)

	return 0;
}

这将输出:

image

现在让我们为这个类添加一个列表构造函数:

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

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

	// We've added a list constructor
	Foo(std::initializer_list<int>)
	{
		std::cout << "Foo(std::initializer_list<int>)" << '\n';
	}

};

int main()
{
	// note that the following statement has not changed
	Foo f1{ 1, 2 }; // now calls Foo(std::initializer_list<int>)

	return 0;
}

尽管我们没有对程序进行其他修改,但该程序现在会输出:

image

警告
为原本没有列表构造函数的现有类添加此构造函数可能会破坏现有程序。


使用 std::initializer_list 进行类赋值

您还可以通过重载赋值运算符(使其接受 std::initializer_list 参数)来使用 std::initializer_list 为类赋予新值。其工作原理与上述方法类似。我们将在下文的测验解答中展示具体实现示例。

请注意:若实现接受 std::initializer_list 的构造函数,必须确保至少满足以下条件之一:

  1. 提供重载的列表赋值运算符
  2. 提供正确的深度复制赋值运算符
  3. 删除复制赋值运算符

原因如下:考虑以下类(未实现上述任何条件)及其列表赋值语句:

#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) // allow IntArray to be initialized via list initialization
		: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

	~IntArray()
	{
		delete[] m_data;
	}

//	IntArray(const IntArray&) = delete; // to avoid shallow copies
//	IntArray& operator=(const IntArray& list) = delete; // to avoid shallow copies

	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 }; // Here's our list assignment statement

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

	return 0;
}

image

首先,编译器会注意到不存在接受 std::initializer_list 的赋值函数。接着它会寻找其他可用的赋值函数,并发现隐式提供的复制赋值运算符。然而该函数仅在能将初始化列表转换为IntArray时才可使用。由于{ 1, 3, 5, 7, 9, 11 }是std::initializer_list类型,编译器将通过列表构造函数将初始化列表转换为临时IntArray。随后调用隐式赋值运算符,将临时IntArray浅拷贝至目标数组对象。

此时临时IntArray的m_data与array->m_data指向相同地址(因浅拷贝所致)。问题端倪已显现。

赋值语句执行完毕后,临时 IntArray 被销毁。其析构函数会删除临时 IntArray 的 m_data,导致 array->m_data 成为悬空指针。当尝试使用 array->m_data 时(包括数组超出作用域导致 m_data 被析构函数删除的情况),将引发未定义行为。

首先,编译器会注意到不存在接受 std::initializer_list 的赋值函数。接着它会寻找其他可用的赋值函数,并发现隐式提供的复制赋值运算符。然而该函数仅在能将初始化列表转换为IntArray时才可使用。由于{ 1, 3, 5, 7, 9, 11 }是std::initializer_list类型,编译器将通过列表构造函数将初始化列表转换为临时IntArray。随后调用隐式赋值运算符,将临时IntArray浅拷贝至目标数组对象。

此时临时IntArray的m_data与array->m_data指向相同地址(因浅拷贝所致)。问题端倪已显现。

赋值语句执行完毕后,临时 IntArray 被销毁。其析构函数会删除临时 IntArray 的 m_data,导致 array->m_data 成为悬空指针。当尝试使用 array->m_data 时(包括数组超出作用域导致 m_data 被析构函数删除的情况),将引发未定义行为。

最佳实践:
若提供列表构造函数,建议同时提供列表赋值操作。


总结

通过实现带std::initializer_list参数的构造函数,可在自定义类中使用列表初始化。我们还可利用std::initializer_list实现其他需要初始化列表的函数,例如赋值运算符。:

若提供列表构造函数,建议同时提供列表赋值操作。


测验时间

问题 #1

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

以下代码应能运行:

int main()
{
	IntArray array { 5, 4, 3, 2, 1 }; // initializer list
	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;
}

这应该输出:

image

显示解决方案

#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) : // allow IntArray to be initialized via list initialization
		IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
	{
		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);
	}

	~IntArray()
	{
		delete[] m_data;
		// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
	}

	IntArray(const IntArray&) = delete; // to avoid shallow copies
	IntArray& operator=(const IntArray& list) = delete; // to avoid shallow copies

	IntArray& operator=(std::initializer_list<int> list)
	{
		// If the new list is a different size, reallocate it
		int length { static_cast<int>(list.size()) };
		if (length != m_length)
		{
			delete[] m_data;
			m_length = length;
			m_data = new int[list.size()]{};
		}

		// Now initialize our array from the list
		std::copy(list.begin(), list.end(), m_data);

		return *this;
	}

	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 }; // initializer list
	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;
}
posted @ 2026-01-31 07:57  游翔  阅读(1)  评论(0)    收藏  举报