Qianfeng

DON'T WORRY BE HAPPY
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

c++ initializer_list踩坑

Posted on 2021-11-01 17:27  RonnieOS  阅读(2926)  评论(1编辑  收藏  举报

c++11后引入了uniform initialization的概念,按照它说的,任何的初始化操作都可以借由大括号{}搞定。
比如在c++98时代,我们会用:

int i[] = {1,3,4,5,6};  //初始化数组

class Cat
{
public:
  Cat(int old, int weight):old(old),weight(weight){};
  int old, weight;
};
Cat c1(12,32); //初始化类实例

Dog d1= 13; // 参数化式地初始化类实例(单参数)
Dog d2 = d1; // 拷贝构造函数初始化类示例

总之,初始化不同的东西,有各种各样的语法,c++11干脆把所有的初始化都用{}实现吧。

int i[]{1,2,3,4};


Cat c1{12,32};

// Dog类似
Dog d1{d3};
Dog d2{d1};

坑,大坑

似乎一切岁月静好,无脑用大括号就完事了。但考虑下面这样的情况:

vector<int> vi{2,3}; //执行完这句,vi里是2,3还是3,3呢?

c++98的程序员知道vector有个构造函数是这样的:

vector( size_type count, const T& value, const Allocator& alloc = Allocator());

就是说初始化时填入count个value。
那么按照大括号里的东西就是构造函数的参数的逻辑,vi构造完的结果是3,3。
但是实际上是2,3。太坑了,太坑了。

如果我们想弄出3,3。得写成

vector<int> vi(2,3);

怎么一会儿大括号里是构造函数的参数,一会儿又不是,烦死了。

解释

具体原因就是编译器看到被大括号包起来的东西,会将其转化成initializer_list类型,initializer_list也就是相当于个容器。例如:

{1,2,3}  --------> initializer_list<int>

即编译器见到前者,就会把其转换成后者。

如果类没有以initializer_list为参数的构造函数时,那编译器会把initializer_list拆包,这个时候{}()没有任何区别,该调用哪个调用哪个,直接将{}看成()就完事。
但是,如果类有initializer_list为参数的构造函数,根据编译器的机制,编译器就会优先调用该构造函数,把一整包initializer_list直接丢进该构造函数去,让该函数处理。

重点来了,为什么vector vi{2,3} 不等同于 vector vi(2,3)呢?,就是因为c++11后的stl容器类,都实现了以initailizer_list为参数的构造函数。以gcc5.4.0的stl实现为例:

 373       vector(initializer_list<value_type> __l,
 374          const allocator_type& __a = allocator_type())
 375       : _Base(__a)
 376       {
 377     _M_range_initialize(__l.begin(), __l.end(),
 378                 random_access_iterator_tag());
 379       }

既然有这个构造函数,遇见{}时就该调用它,把initializer_list一整包丢进去。而非调用像上面的

vector( size_type count, const T& value, const Allocator& alloc = Allocator());

相关知识

c++11中{}语法的出现,也导致了多实参非explicit ctor构造函数可能会在隐式转换时调用。在以前,只有one argument non-explicit ctor才有可能会在隐式转换时调用,因此我们加个explicit就阻止了这种转换。但是c++11后编译器会将{}自动解包(如果没有initializer_list为参的构造函数时),可能解包出来多个东西。因此将multi-argument ctor前面加上explicit就变得有必要了。

一些bb

我觉得这个设计有些问题,当用第三方类库的时候,鬼知道它有没有写initializer_list为参的构造函数。本来我只是想开开心心用{}的语法调用普通构造函数,结果不小心调用了initializer_list的版本,佛了。感情我写{}之前还得看看类库的源代码是吧。
最后我想出了一个解决办法,估计也是设计者想让我们用的吧,就是正常创建类对象时,用()就完事。只有真正当我知道该类(例如stl容器)提供了初始化列表的版本,并且我知道其行为,并且我真的需要它的时候,我才用{}