大括号之谜:C++的列表初始化语法解析
轉(zhuǎn)載: https://segmentfault.com/a/1190000039362151
摘要:有朋友在使用std::array時發(fā)現(xiàn)一個奇怪的問題:當(dāng)元素類型是復(fù)合類型時,編譯通不過。
有朋友在使用std::array時發(fā)現(xiàn)一個奇怪的問題:當(dāng)元素類型是復(fù)合類型時,編譯通不過。
struct?S?{int?x;int?y; };int?main() {int?a1[3]{1,?2,?3};??//?簡單類型,原生數(shù)組std::array<int,?3>?a2{1,?2,?3};??//?簡單類型,std::arrayS?a3[3]{{1,?2},?{3,?4},?{5,?6}};??//?復(fù)合類型,原生數(shù)組std::array<S, 3> a4{{1, 2}, {3, 4}, {5, 6}};??//?復(fù)合類型,std::array,編譯失敗!return?0; }按說std::array和原生數(shù)組的行為幾乎是一樣的,可為什么當(dāng)元素類型不同時,初始化語法還會有差別?更蹊蹺的是,如果多加一層括號,或者去掉內(nèi)層的括號,都能讓代碼編譯通過:
std::array<S, 3> a1{{1, 2}, {3, 4}, {5, 6}};??//?原生數(shù)組的初始化寫法,編譯失敗! std::array<S,?3>?a2{{{1,?2},?{3,?4},?{5,?6}}};??//?外層多一層括號,編譯成功 std::array<S,?3>?a3{1,?2,?3,?4,?5,?6};??//?內(nèi)層不加括號,編譯成功這篇文章會介紹這個問題的原理,以及正確的解決方式。
聚合初始化
先從std::array的內(nèi)部實(shí)現(xiàn)說起。為了讓std::array表現(xiàn)得像原生數(shù)組,C++中的std::array與其他STL容器有很大區(qū)別——std::array沒有定義任何構(gòu)造函數(shù),而且所有內(nèi)部數(shù)據(jù)成員都是public的。這使得std::array成為一個聚合(aggregate)。
對聚合的定義,在每個C++版本中有少許的區(qū)別,這里簡單總結(jié)下C++17中定義:一個class或struct類型,當(dāng)它滿足以下條件時,稱為一個聚合[1]:
沒有private或protected數(shù)據(jù)成員;
沒有用戶提供的構(gòu)造函數(shù)(但是顯式使用**=default或=delete**聲明的構(gòu)造函數(shù)除外);
沒有virtual、private或者protected基類;
沒有虛函數(shù)
直觀的看,聚合常常對應(yīng)著只包含數(shù)據(jù)的struct類型,即常說的POD類型。另外,原生數(shù)組類型也都是聚合。
聚合初始化可以用大括號列表。一般大括號內(nèi)的元素與聚合的元素一一對應(yīng),并且大括號的嵌套也和聚合類型嵌套關(guān)系一致。在C語言中,我們常見到這樣的struct初始化語句。
解了上面的原理,就容易理解為什么std::array的初始化在多一層大括號時可以成功了——因?yàn)?strong>std::array內(nèi)部的唯一元素是一個原生數(shù)組,所以有兩層嵌套關(guān)系。下面展示一個自定義的MyArray類型,它的數(shù)據(jù)結(jié)構(gòu)和std::array幾乎一樣,初始化方法也類似:
struct?S?{int?x;int?y; };template<typename?T,?size_t?N> struct?MyArray?{T?data[N]; };int?main() {MyArray<int,?3>?a1{{1,?2,?3}};??//?兩層大括號MyArray<S,?3>?a2{{{1,?2},?{3,?4},?{5,?6}}};??//?三層大括號return?0; }在上面例子中,初始化列表的最外層大括號對應(yīng)著MyArray,之后一層的大括號對應(yīng)著數(shù)據(jù)成員data,再之后才是data中的元素。大括號的嵌套與類型間的嵌套完全一致。這才是std::array嚴(yán)格、完整的初始化大括號寫法。
可是,為什么當(dāng)std::array元素類型是簡單類型時,省掉一層大括號也沒問題?——這就涉及聚合初始化的另一個特點(diǎn):大括號省略。
大括號省略(brace elision)
C++允許在聚合的內(nèi)部成員仍然是聚合時,省掉一層或多層大括號。當(dāng)有大括號被省略時,編譯器會按照內(nèi)層聚合所含的元素個數(shù)進(jìn)行依次填充。
下面的代碼雖然不常見,但是是合法的。雖然二維數(shù)組初始化只用了一層大括號,但因?yàn)榇罄ㄌ柺÷蕴匦?#xff0c;編譯器會依次用所有元素填充內(nèi)層數(shù)組——上一個填滿后再填下一個。
int?a[3][2]{1,?2,?3,?4,?5,?6};?//?等同于{{1,?2},?{3,?4},?{5,?6}}知道了大括號省略后,就知道std::array初始化只用一層大括號的原理了:由于std::array的內(nèi)部成員數(shù)組是一個聚合,當(dāng)編譯器看到**{1,2,3}這樣的列表時,會挨個把大括號內(nèi)的元素填充給內(nèi)部數(shù)組的元素。甚至,假設(shè)std::array**內(nèi)部有兩個數(shù)組的話,它還會在填完上一個數(shù)組后依次填下一個。
這也解釋了為什么省掉內(nèi)層大括號,復(fù)雜類型也可以編譯成功:
std::array<S,?3>?a3{1,?2,?3,?4,?5,?6};?//?內(nèi)層不加括號,編譯成功因?yàn)?strong>S也是個聚合類型,所以這里省略了兩層大括號。編譯期按照下面的順序依次填充元素:數(shù)組0號元素的S::x、數(shù)組0號元素的S::y、數(shù)組1號元素的S::x、數(shù)組1號元素的S::y……
雖然大括號可以省略,但是一旦用戶顯式的寫出了大括號,那么必須要和這一層的元素個數(shù)嚴(yán)格對應(yīng)。因此下面的寫法會報錯:
std::array<S, 3> a1{{1, 2}, {3, 4}, {5, 6}};?//?編譯失敗!編譯器認(rèn)為**{1,2}對應(yīng)std::array的內(nèi)部數(shù)組,然后{3,4}對應(yīng)std::array的下一個內(nèi)部成員。可是std::array**只有一個數(shù)據(jù)成員,于是報錯:too many initializers for 'std::array<S, 3>'
需要注意的是,大括號省略只對聚合類型有效。如果S有個自定義的構(gòu)造函數(shù),省掉大括號就行不通了:
//?聚合 struct?S1?{S1()?=?default;int?x;int?y; };std::array<S1,?3>?a1{1,?2,?3,?4,?5,?6};??//?OK//?聚合 struct?S2?{S2()?=?delete;int?x;int?y; };std::array<S2,?3>?a2{1,?2,?3,?4,?5,?6};??//?OK//?非聚合,有用戶提供的構(gòu)造函數(shù) struct?S3?{S3()?{};int?x;int?y; };std::array<S3, 3> a3{1, 2, 3, 4, 5, 6};??//?編譯失敗!這里可以看出**=default**的構(gòu)造函數(shù)與空構(gòu)造函數(shù)的微妙區(qū)別。
std::initializer_list的另一個故事
上面講的所有規(guī)則,都只對聚合初始化有效。如果我們給MyArray類型加上一個接受std::initializer_list的構(gòu)造函數(shù),情況又不一樣了:
struct?S?{int?x;int?y; };template<typename?T,?size_t?N> struct?MyArray?{ public:MyArray(std::initializer_list<T>?l){std::copy(l.begin(),?l.end(),?std::begin(data));}T?data[N]; };int?main() {MyArray<S,?3>?a{{{1,?2},?{3,?4},?{5,?6}}};??//?OKMyArray<S,?3>?b{{1,?2},?{3,?4},?{5,?6}};??//?同樣OKreturn?0; }當(dāng)使用std::initializer_list的構(gòu)造函數(shù)來初始化時,無論初始化列表外層是一層還是兩層大括號,都能初始化成功,而且a和b的內(nèi)容完全一樣。
這又是為什么?難道std::initializer_list也支持大括號省略?
這里要提一件趣事:**《Effective Modern C++》**這本書在講解對象初始化方法時,舉了這么一個例子[2]:
class?Widget?{ public:Widget();???????????????????????????????????//?default?ctorWidget(std::initializer_list<int>?il);??????//?std::initializer_list?ctor…??????????????????????????????????????????//?no?implicit?conversion?funcs };?Widget?w1;??????????//?calls?default?ctor Widget?w2{};????????//?also?calls?default?ctor Widget?w3();????????//?most?vexing?parse!?declares?a?function!????Widget?w4({});??????//?calls?std::initializer_list?ctor?with?empty?list Widget w5{{}};??????// ditto <-注意!然而,書里這段代碼最后一行w5的注釋卻是個技術(shù)錯誤。這個w5的構(gòu)造函數(shù)調(diào)用時并非像w4那樣傳入一個空的std::initializer_list,而是傳入包含了一個元素的std::initializer_list。
即使像Scott Meyers這樣的C++大牛,都會在大括號的語義上搞錯,可見C++的相關(guān)規(guī)則充滿著陷阱!
連《Effective Modern C++》都弄錯了的規(guī)則
幸好,《Effective Modern C++》作為一本經(jīng)典圖書,讀者眾多。很快就有讀者發(fā)現(xiàn)了這個錯誤,之后Scott Meyers將這個錯誤的闡述放在了書籍的勘誤表中[3]。
Scott Meyers還邀請讀者們和他一起研究正確的規(guī)則到底是什么,最后,他們把結(jié)論寫在了一篇文章里[4]。文章通過3種具有不同構(gòu)造函數(shù)的自定義類型,來揭示std::initializer_list匹配時的微妙差異。代碼如下:
#include?<iostream> #include?<initializer_list>class?DefCtor?{int?x; public:DefCtor(){} };class?DeletedDefCtor?{int?x; public:DeletedDefCtor()?=?delete; };class?NoDefCtor?{int?x;???? public:NoDefCtor(int){} };template<typename?T> class?X?{ public:X()?{?std::cout?<<?"Def?Ctorn";?}X(std::initializer_list<T>?il){std::cout?<<?"il.size()?=?"?<<?il.size()?<<?'n';} };int?main() {X<DefCtor>?a0({});???????????//?il.size?=?0X<DefCtor>?b0{{}};???????????//?il.size?=?1X<DeletedDefCtor>?a2({});????//?il.size?=?0//?X<DeletedDefCtor>?b2{{}};????//?error!?attempt?to?use?deleted?constructorX<NoDefCtor>?a1({});?????????//?il.size?=?0X<NoDefCtor>?b1{{}};?????????//?il.size?=?0 }對于構(gòu)造函數(shù)已被刪除的非聚合類型,用**{}初始化會觸發(fā)編譯錯誤,因此b2的表現(xiàn)是容易理解的。但是b0和b1的區(qū)別就很奇怪了:一模一樣的初始化方法,為什么一個傳入std::initializer_list**的長度為1,另一個長度為0?
構(gòu)造函數(shù)的兩步嘗試
問題的原因在于:當(dāng)使用大括號初始化來調(diào)用構(gòu)造函數(shù)時,編譯器會進(jìn)行兩次嘗試:
把整個大括號列表連同最外層大括號一起,作為構(gòu)造函數(shù)的std::initializer_list參數(shù),看看能不能匹配成功;
如果第一步失敗了,則將大括號列表的成員作為構(gòu)造函數(shù)的入?yún)?#xff0c;看看能不能匹配成功。
對于b0{{}}這樣的表達(dá)式,可以直觀理解第一步嘗試是:b0({{}}),也就是把{{}}整體作為一個參數(shù)傳給構(gòu)造函數(shù)。對b0來說,這個匹配是能夠成功的。因?yàn)?strong>DefCtor可以通過**{}初始化,所以b0的初始化調(diào)用了X(std::initializer_list),并且傳入含有1個成員的std::initializer_list**作為入?yún)ⅰ?/p>
對于b1{{}},編譯器同樣會先做第一步嘗試,但是NoDefCtor不允許用**{}初始化,所以第一步嘗試會失敗。接下來編譯器做第二步嘗試,將外層大括號剝掉,調(diào)用b1({}),發(fā)現(xiàn)可以成功,這時傳入的是空的std::initializer_list**。
再回頭看之前MyArray的例子,現(xiàn)在我們可以分析出兩種初始化分別是在哪一步成功的:
MyArray<S,?3>?a{{{1,?2},?{3,?4},?{5,?6}}};??//?在第二步,剝掉外層大括號后匹配成功 MyArray<S,?3>?b{{1,?2},?{3,?4},?{5,?6}};??//?第一步整個大括號列表匹配成功綜合小測試
到這里,大括號初始化在各種場景下的規(guī)則就都解析完了。不知道讀者是否徹底掌握了?
不妨來試一試下面的小測試:這段代碼里有一個僅含一個元素的std::array,其元素類型是std::tuple,tuple只有一個成員,是自定義類型S,S定義有默認(rèn)構(gòu)造函數(shù)和接受**std::initializer_list**的構(gòu)造函數(shù)。對于這個類型,初始化時允許使用幾層大括號呢?下面的初始化語句有哪些可以成功?分別是為什么?
struct?S?{S()?=?default;S(std::initializer_list<int>)?{} };int?main() {using?MyType?=?std::array<std::tuple<S>,?1>;MyType?a{};?????????????//?1層MyType?b{{}};???????????//?2層MyType?c{{{}}};?????????//?3層MyType?d{{{{}}}};???????//?4層MyType?e{{{{{}}}}};?????//?5層MyType?f{{{{{{}}}}}};???//?6層MyType?g{{{{{{{}}}}}}};?//?7層return?0; }尾注
[1] https://en.cppreference.com/w/cpp/language/aggregate_initialization?
[2] 位于書的 Item 7: Distinguish between () and {} when creating objects. 第55頁?
[3] https://www.aristeia.com/BookErrata/emc++-errata.html
[4] https://scottmeyers.blogspot.com/2016/11/help-me-sort-out-meaning-of-as.html
總結(jié)
以上是生活随笔為你收集整理的大括号之谜:C++的列表初始化语法解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux莫烦笔记
- 下一篇: 国密SM2非对称加密算法(对本地文件的加