解析之C++的列表初始化语法
聚合初始化先从std::array的内部实现说起。为了让std::array表现得像原生数组C中的std::array与其他STL容器有很大区别——std::array没有定义任何构造函数而且所有内部数据成员都是public的。这使得std::array成为一个聚合aggregate。对聚合的定义在每个C版本中有少许的区别这里简单总结下C17中定义一个class或struct类型当它满足以下条件时称为一个聚合[1]没有private或protected数据成员没有用户提供的构造函数但是显式使用default或delete声明的构造函数除外没有virtual、private或者protected基类没有虚函数直观的看聚合常常对应着只包含数据的struct类型即常说的POD类型。另外原生数组类型也都是聚合。聚合初始化可以用大括号列表。一般大括号内的元素与聚合的元素一一对应并且大括号的嵌套也和聚合类型嵌套关系一致。在C语言中我们常见到这样的struct初始化语句。解了上面的原理就容易理解为什么std::array的初始化在多一层大括号时可以成功了——因为std::array内部的唯一元素是一个原生数组所以有两层嵌套关系。下面展示一个自定义的MyArray类型它的数据结构和std::array几乎一样初始化方法也类似12345678910111213141516structS {intx;inty;};templatetypenameT,size_tNstructMyArray {T data[N];};intmain(){MyArrayint, 3 a1{{1, 2, 3}};// 两层大括号MyArrayS, 3 a2{{{1, 2}, {3, 4}, {5, 6}}};// 三层大括号return0;}在上面例子中初始化列表的最外层大括号对应着MyArray之后一层的大括号对应着数据成员data再之后才是data中的元素。大括号的嵌套与类型间的嵌套完全一致。这才是std::array严格、完整的初始化大括号写法。可是为什么当std::array元素类型是简单类型时省掉一层大括号也没问题——这就涉及聚合初始化的另一个特点大括号省略。大括号省略brace elisionC允许在聚合的内部成员仍然是聚合时省掉一层或多层大括号。当有大括号被省略时编译器会按照内层聚合所含的元素个数进行依次填充。下面的代码虽然不常见但是是合法的。虽然二维数组初始化只用了一层大括号但因为大括号省略特性编译器会依次用所有元素填充内层数组——上一个填满后再填下一个。1inta[3][2]{1, 2, 3, 4, 5, 6};// 等同于{{1, 2}, {3, 4}, {5, 6}}知道了大括号省略后就知道std::array初始化只用一层大括号的原理了由于std::array的内部成员数组是一个聚合当编译器看到{1,2,3}这样的列表时会挨个把大括号内的元素填充给内部数组的元素。甚至假设std::array内部有两个数组的话它还会在填完上一个数组后依次填下一个。这也解释了为什么省掉内层大括号复杂类型也可以编译成功1std::arrayS, 3 a3{1, 2, 3, 4, 5, 6};// 内层不加括号编译成功因为S也是个聚合类型所以这里省略了两层大括号。编译期按照下面的顺序依次填充元素数组0号元素的S::x、数组0号元素的S::y、数组1号元素的S::x、数组1号元素的S::y……虽然大括号可以省略但是一旦用户显式的写出了大括号那么必须要和这一层的元素个数严格对应。因此下面的写法会报错1std::arrayS, 3 a1{{1, 2}, {3, 4}, {5, 6}};// 编译失败编译器认为{1,2}对应std::array的内部数组然后{3,4}对应std::array的下一个内部成员。可是std::array只有一个数据成员于是报错too many initializers for std::arrayS, 3需要注意的是大括号省略只对聚合类型有效。如果S有个自定义的构造函数省掉大括号就行不通了1234567891011121314151617181920212223242526// 聚合structS1 {S1() default;intx;inty;};std::arrayS1, 3 a1{1, 2, 3, 4, 5, 6};// OK// 聚合structS2 {S2() delete;intx;inty;};std::arrayS2, 3 a2{1, 2, 3, 4, 5, 6};// OK// 非聚合有用户提供的构造函数structS3 {S3() {};intx;inty;};std::arrayS3, 3 a3{1, 2, 3, 4, 5, 6};// 编译失败这里可以看出default的构造函数与空构造函数的微妙区别。std::initializer_list的另一个故事上面讲的所有规则都只对聚合初始化有效。如果我们给MyArray类型加上一个接受std::initializer_list的构造函数情况又不一样了123456789101112131415161718192021structS {intx;inty;};templatetypenameT,size_tNstructMyArray {public:MyArray(std::initializer_listT l){std::copy(l.begin(), l.end(), std::begin(data));}T data[N];};intmain(){MyArrayS, 3 a{{{1, 2}, {3, 4}, {5, 6}}};// OKMyArrayS, 3 b{{1, 2}, {3, 4}, {5, 6}};// 同样OKreturn0;}当使用std::initializer_list的构造函数来初始化时无论初始化列表外层是一层还是两层大括号都能初始化成功而且a和b的内容完全一样。这又是为什么难道std::initializer_list也支持大括号省略这里要提一件趣事《Effective Modern C》这本书在讲解对象初始化方法时举了这么一个例子[2]12345678910111213classWidget {public:Widget();// default ctorWidget(std::initializer_listint il);// std::initializer_list ctor…// no implicit conversion funcs};Widget w1;// calls default ctorWidget w2{};// also calls default ctorWidget w3();// most vexing parse! declares a function!Widget w4({});// calls std::initializer_list ctor with empty listWidget w5{{}};// ditto -注意然而书里这段代码最后一行w5的注释却是个技术错误。这个w5的构造函数调用时并非像w4那样传入一个空的std::initializer_list而是传入包含了一个元素的std::initializer_list。即使像Scott Meyers这样的C大牛都会在大括号的语义上搞错可见C的相关规则充满着陷阱
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2568155.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!