一、列表初始化和初始化列表
对于使用大括号{}进行初始化,在前面反复分析过,给出的建议是可能的情况下尽量使用大括号进行初始化。但这并不意味着大括号是完美的初始化方式。
这里要显式的说明一下大括号{}和std::initializer_list两种机制。从形式让讲前面的大括号叫列表初始化,而后面的std::initializer_list可以叫做初始化列表。而本质上,std::initializer_list作为c++11引入的轻量级的模板类,专门用于支持前者列表初始化。
这是一件好事,但可能在某些情况下,好事也会变成不好的事。注意,这里没说是坏事。
这里需要注意std::initializer_list的几个特点即只读性(内部元素视为const)、不支持移动语义、浅拷贝以及类型统一要求。看看这几点,哪一点单独拿出来都是在走钢丝。所以,它的问题,一定会在使用不当的时候暴露出来。
二、主要的问题
对于使用大括号进行初始化,除了前面提到的窄化转换问题,其它还有几个重要的问题需要说明。
主要包括:
1、initializer_list构造函数优先级过高
对于一些类中定义了支持std::initializer_list的构造函数时,编译器会优先选择更匹配std::initializer_list的构造函数,这是最容易产生意外行为的问题。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v1(10, 9); // 10个元素,每个都是9
std::vector<int> v2{10, 9}; // 两个元素:10和9
std::cout << v1.size() << std::endl; // 10
std::cout << v2.size() << std::endl; // 2
}
如果一个类同时有普通构造函数和 initializer_list构造函数,使用 {}时会优先尝试后者:
#include <initializer_list>
#include <iostream>
class demo {
public:
demo(int count, int value)
{
std::cout << "普通构造函数"<<std::endl;
}
demo(std::initializer_list<int>)
{
std::cout << "initializer_list构造函数"<<std::endl;
}
};
int main()
{
demo a(10, 9); // 普通构造函数
demo b{10, 9}; // initializer_list构造函数
}
遇到这一类问题,解决的首先是判定是否支持initializer_list,特别是对stl的库容器等,要严格小心注意。也可以从应用层出发,对于参数设置,则直接调用构造函数即使用小括号的机制;而对于一组元素的初始化时,则使用大括号。另外,如果是自写编写相关类,没有显式的要求下,不要提供initializer_list的构造函数。
2、initializer_list元素只读,不能拷贝
在前面的注意点中提到了std::initializer_list 的元素类型相当于 const t。
所以在一些需要移动结合应用的情况下,代码就会出现问题:
#include <memory>
#include <vector>
std::vector<std::unique_ptr<int>> data{
std::make_unique<int>(1),
std::make_unique<int>(2)
};
大家都知道,unique_ptr只可移动不可复制,而列表中的元素是 const,意味着无法进行移动。
要想实现这个效果则可以使用:
std::vector<std::unique_ptr<int>> data; data.reserve(2); data.push_back(std::make_unique<int>(1)); data.push_back(std::make_unique<int>(2));
当然,这里更建议使用 emplace_back,但资源所有权场景通常优先使用 make_unique。
3、initializer_list的生命周期问题
std::initializer_list可以看作一种底层数据的上层 的view,所以不要返回指向临时列表底层数组的 initializer_list。
这和不要返回临时变量的指针有些类似:
#include <initializer_list>
std::initializer_list<int> getdata()
{
return {3, 6, 9}; // 危险:返回后底层数组已经失效
}
应返回拥有数据所有权的容器,其实就是进行了拷贝:
#include <array>
std::array<int, 3> getdata()
{
return {3, 6, 9};
}
//或
#include <vector>
std::vector<int> getdata()
{
return {3, 6, 9};
}
不过如果可以保障其安全的作用域,是可以正常使用的。比如作为函数参数临时使用通常是安全的:
void print(std::initializer_list<int> data);
print({3, 6, 9}); // 列表在整个函数调用期间有效
但是如果对列表的 begin()等指针进行保存并在函数调用结束后继续使用,仍然会出现问题。
4、模板参数的推导
在前面学习ctad时,模板可以自动进行模板参数的推导。
但如果是从单纯的{}往往推导无法成功,如下:
template<class t>
void set(const t& v){}
// set({3, 3, 3}); // 编译错误:无法推导 t
这时可以显式的构造对象类型,以供推导。如:
#include <vector>
set(std::vector<int>{3, 3, 3});
或者增加重载函数,使用std::initializer_list:
#include <initializer_list>
template<class t>
void set(std::initializer_list<t> v){}
int main()
{
set({3, 3, 3}); // t 推导为 int
}
同样,下面的代码一般也无法自动推导:
std::make_unique<std::vector<int>>({3, 3, 3}); // 无法推导
和上面一样,或者使用initializer_list或者预先显式的构造容器:
auto p = std::make_unique<std::vector<int>>(
std::initializer_list<int>{3, 3, 3}
);
auto p = std::make_unique<std::vector<int>>(
std::vector<int>{3, 3, 3}
);
5、列表初始化的应用方式对explicit构造的影响
对于拥有显式的explicit构造函数的类,不能使用隐式的调用它。
这在大括号初始化中同样适用:
class demo {
public:
explicit demo(int value) {}
};
demo d{10}; // ok:直接列表初始化
// demo d1 = {10}; // err:隐式调用 explicit构造函数
正如例程中一样,只能是显式的调用构造函数:
demo d{10};
在通过函数传递参数时,也应如此:
void set(demo d);
set(demo{10});
6、空列表可能导致歧义
看下面的代码:
void set(std::vector<int>);
void set(std::string);
int main()
{
// set({}); // err:重载匹配错误
}
处理这种问题,只能是显式的给出调用参数的数据类型:
set(std::vector<int>{});
set(std::string{});
7、聚合类型初始化的顺序问题
在新的标准中提供了聚合类型的初始化,但顺序的依赖性也由此产生,不能跳着初始化或个别不初始化。如下:
struct data
{
int id;
char* name;//char name[100];
};
data d{31,"tom"};
如果换顺序则可能会出现各种问题。如果想忽略某个值的初始化,可使用c++20中的指定初始化,但其仍然需要严格按顺序进行。
8、对auto支持的问题
有个c++大牛强力推荐使用auto。但它在遇到初始化列表时,也要注意。
看下面的代码:
auto a{1}; // int
auto b = {2}; // std::initializer_list<int>
auto c = {3, 4}; // std::initializer_list<int>
// auto d{5, 6}; // err:直接列表初始化只能有一个元素
// auto e = {7, 8.0}; // err:元素类型不统一
c/c++天然对int支持的好啊。没办法,胎里带。处理这类问题,还是显式的给出类型。不再给出例子。
三、应用说明
在明白了上述大括号初始化的细节问题后,其实就也有一个清晰的正确使用的方式:
- 基本类型和聚合类型初始化,推荐使用列表初始化
- 要严格处理拥有initializer_list构造函数的类,特别注意参数设置和元素初始化。比如stl中的向量等
- initializer_list不可应用于不可复制对象的批量初始化
- 不可扩大initializer_list相关数据的应用范围
- 要严格限制在template,auto,overload三种情况下的应用,显式明确相关对象的类型列表
总体上来说,使用大括号这种列表初始化的机制,统一、方便。仍然是对象初始化的优先使用方式,但在使用方便时,也要看到其中细节的问题,不能一头栽进去,出了问题还不知道为什么。
四、总结
在任何情况下都不要想当然,都要严格的按标准行事。扩大影响或使用范围,都需要当前环境或工具的显式支持。只有这样,才可能写出安全稳定的c++代码。
列表初始化作为一个被广泛推荐的初始化方式,仍然在c++中存在着这样那样的细节上的陷阱。所以,对于c++开发者来说,不能轻易的滥用任务技术。自己的大脑中始终要保持清醒和警惕!
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论