1.任意长度的初始化列表
读者可能注意到了,C++11中的stl容器拥有和未显示指定长度的数组一样的初始化能力,代码如下:
int arr[] { 1, 2, 3 }; std::map<std::string, int> mm = { { "1", 1 }, { "2", 2 }, { "3", 3 } }; std::set<int> ss = { 1, 2, 3 }; std::vector<int> arr = { 1, 2, 3, 4, 5 };
这里arr没有显式指定长度,因此,它的初始化列表可以是任意长度。
同样,std::map、std::set、std::vector也可以在初始化时任意书写需要初始化的内容。
前面自定义的Foo却不具备这种能力,只能按部就班地按照构造函数指定的参数列表进行赋值。
实际上,stl中的容器是通过使用std::initializer_list这个轻量级的类模板来完成上述功能支持的。我们只需要为Foo添加一个std::initializer_list构造函数,它也将拥有这种任意长度初始化的能力,代码如下:
class Foo { public: Foo(std::initializer_list<int>) {} }; Foo foo = { 1, 2, 3, 4, 5 }; // OK!
那么,知道了使用std::initializer_list来接收{...},如何通过它来给自定义容器赋值呢?来看代码清单1-9中的例子。
代码清单1-9 通过std::initializer_list给自定义容器赋值示例
class FooVector { std::vector<int> content_; public: FooVector(std::initializer_list<int> list) { for (auto it = list.begin(); it != list.end(); ++it) { content_.push_back(*it); } } }; class FooMap { std::map<int, int> content_; using pair_t = std::map<int, int>::value_type; public: FooMap(std::initializer_list<pair_t> list) { for (auto it = list.begin(); it != list.end(); ++it) { content_.insert(*it); } } }; FooVector foo_1 = { 1, 2, 3, 4, 5 }; FooMap foo_2 = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
这里定义了两个自定义容器,一个是FooVector,采用std::vector<int>作为内部存储;另一个是FooMap,采用std::map<int, int>作为内部存储。
可以看到,FooVector、FooMap的初始化过程,就和它们使用的内部存储结构一样。
这两个自定义容器的构造函数中,std::initializer_list负责接收初始化列表。并通过我们熟知的for循环过程,把列表中的每个元素取出来,并放入内部的存储空间中。
std::initializer_list不仅可以用来对自定义类型做初始化,还可以用来传递同类型的数据集合,代码如下:
void func(std::initializer_list<int> l) { for (auto it = l.begin(); it != l.end(); ++it) { std::cout << *it << std::endl; } } int main(void) { func({}); // 一个空集合 func({ 1, 2, 3 }); // 传递 { 1, 2, 3 } return 0; }
如上述所示,在任何需要的时候,std::initializer_list都可以当作参数来一次性传递同类型的多个数据。
2.std::initializer_list的一些细节
了解了std::initializer_list之后,再来看看它的一些特点,如下:
它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。
对于std::initializer_list<T>而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型T(或可转换为T)。
它有3个成员接口:size()、begin()、end()。
它只能被整体初始化或赋值。
通过前面的例子,已经知道了std::initializer_list的前几个特点。其中没有涉及的接口size()是用来获得std::initializer_list的长度的,比如:
std::initializer_list<int> list = { 1, 2, 3 };
size_t n = list.size(); // n == 3
最后,对std::initializer_list的访问只能通过begin()和end()进行循环遍历,遍历时取得的迭代器是只读的。因此,无法修改std::initializer_list中某一个元素的值,但是可以通过初始化列表的赋值对std::initializer_list做整体修改,代码如下:
std::initializer_list<int> list; size_t n = list.size(); // n == 0 list = { 1, 2, 3, 4, 5 }; n = list.size(); // n == 5 list = { 3, 1, 2, 4 }; n = list.size(); // n == 4
std::initializer_list拥有一个无参数的构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list。
之后,我们对std::initializer_list进行赋值操作(注意,它只能通过初始化列表赋值),可以发现std::initializer_list被改写成了{1, 2, 3, 4, 5}。
然后,还可以对它再次赋值,std::initializer_list被修改成了{3, 1, 2, 4}。
看到这里,可能有读者会关心std::initializer_list的传递或赋值效率。
假如std::initializer_list在传递或赋值的时候如同vector之类的容器一样,把每个元素都复制了一遍,那么使用它传递类对象的时候就要斟酌一下了。
实际上,std::initializer_list是非常高效的。它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了列表中元素的引用而已。
因此,我们不应该像这样使用:
std::initializer_list<int> func(void) { int a = 1, b = 2; return { a, b }; // a、b在返回时并没有被拷贝 }
虽然这能够正常通过编译,但却无法传递出我们希望的结果(a、b在函数结束时,生存期也结束了,因此,返回的将是不确定的内容)。
这种情况下最好的做法应该是这样:
std::vector<int> func(void) { int a = 1, b = 2; return { a, b }; }
使用真正的容器,或具有转移/拷贝语义的物件来替代std::initializer_list返回需要的结果。
我们应当总是把std::initializer_list看做保存对象的引用,并在它持有对象的生存期结束之前完成传递。