查看原文
其他

Uniform Initialization and initializer_list

cpluspluser CppMore 2023-04-20

C++初始化数据的方式有许多种,例如:

1void func()
2
{
3    int a = 1;
4    int b{1};
5    int c(1);
6    int d = int(1);
7}

我们可以通过assignment初始化,也能通过小括号初始化,还能通过花括号初始化。

这对于新手来说并不友好,常常会使人迷惑到底该怎样初始化一个变量或对象。

因此,C++11引进了一个概念,称为Uniform Initialization(一致性初始化)。对于任何变量或对象,你都可以使用一个共有的语法,这个语法就是使用{}初始化。

所以现在的初始化都可以这样写:

1std::vector<int> vec1 { 123456 };
2std::vector<std::string> vec2 { "Have""a""nice""day""!"};
3
4print_vector(vec1);  // output: 1 2 3 4 5 6
5print_vector(vec2);  // output: Have a nice day !

而这个花括号,也就是{ }所组成的值的类型,就是initializer_list。

编译器在遇到{ 1, 2, 3, 4, 5, 6 }时,便会生成一个initializer_list,若函数提供了initializer_list类型的参数,那么所生成的initializer_list便会直接传入该函数;若函数未提供该类型的参数,那么编译器会将元素逐一分解,传给函数。

比如遇到 { "Have", "a", "nice", "day", "!" },编译器就会创建一个initializer_list,而在initializer_list的内部,其实用的是array,所以实际类型就是array<string, 5>,由于vector提供有initializer_list类型的构造函数,因此可以直接初始化。,>

然而initializer_list拥有高优先级,这往往会掩盖一些重载决议,而这些重载决议又极其容易让程序员误用。

vector就存在着这样的一个小bug,比如看下面这句代码:

1std::vector<int> vec3 { 12 };

这的确算是一个bug,也许你在工作中已经踩过几次这个坑了。

若你还未看懂问题之所在,那么你可能不知道写这句代码的本意是要干嘛。

对比下面这句代码:

1std::vector<int> vec4(12);

现在,你应该清楚vec3的问题了。

vec3的本意是要放1个2,就如同vec4那样。而由于initializer_list的高优先级,vec3实际放了1和2两个数。

就如同nullptr和0要区分一样,对于这样的模糊语义,编译器至少应该明确地给出重载模糊警告,以防止误用。

要想正确使用initializer_list,就得明确这些细节,因此,我们定义一个Point类来观察其行为:

1template <typename T>
2struct Point
3{

4    Point(T x, T y = T())
5    {
6        std::cout << "Point(T x, T y)\n" << x << ' ' << y << std::endl;
7    }
8
9    Point(std::initializer_list<T> init_list)
10    {
11        std::cout << "Point(std::initializer_list<T>)\n";
12        for (auto elem : init_list)
13            std::cout << elem << ' ';
14        std::cout << std::endl;
15    }
16
17    Point(T x, T y, T z) {
18        std::cout << "Point(T x, T y, T z)\n" << x << ' ' << y << ' ' << z << std::endl;
19    }
20};

Point有三个构造函数,第一个支持单类型实参,可以接受一个或两个参数;第二个支持initializer_list,可以接受任意个参数;第三个支持三个参数。

现在,构造实验数据,

1Point<int> p1{ 1 };
2Point<int> p2{ 12 };
3Point<int> p3{ 123 };
4Point<int> p4{ 1234 };
5Point<int> p5 = { 1 };
6Point<int> p6 = { 12 };
7Point<int> p7 = { 123 };
8Point<int> p8 = { 1234 };
9Point<int> p9(1);
10Point<int> p10(12);
11Point<int> p11(123);
12//! Point<int> p12(1, 2, 3, 4);

除了p12,其它都有相匹配的构造函数。

那么它们最终会匹配到哪个版本呢?

输出如下:

使用{}初始化的对象,全部都会调用initializer_list参数的函数。

而单参,两参,三参的构造函数我们都专门提供的有,按理说就应该调用专门提供的版本,而在这种语义模糊的情境下,并没有任何的提示。因此,当使用initializer_list作为函数参数时,需要留心这个特性。

此外,initializer_list应对的也是变化问题,普通类型的函数只能支持固定个数的参数,而其可接受任意个数的参数。问题一旦是动态的,便不容易解决,这和Variadic Templates(可变参模板)有相似之处,只不过前者只支持相同类型的参数,而后者还支持不同类型的参数。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存