C++的列表初始化語法解析

有朋友在使用std::array時(shí)發(fā)現(xiàn)一個(gè)奇怪的問題:當(dāng)元素類型是復(fù)合類型時(shí),編譯通不過。

struct S {

? ? int x;

? ? int y;

};int main()

{

? ? inta1[3]{1,2,3};// 簡(jiǎn)單類型,原生數(shù)組std::array a2{1,2,3};// 簡(jiǎn)單類型,std::arrayS a3[3]{{1,2}, {3,4}, {5,6}};// 復(fù)合類型,原生數(shù)組std::array a4{{1,2}, {3,4}, {5,6}};// 復(fù)合類型,std::array,編譯失敗!return0;

}

按說std::array和原生數(shù)組的行為幾乎是一樣的,可為什么當(dāng)元素類型不同時(shí),初始化語法還會(huì)有差別?更蹊蹺的是,如果多加一層括號(hào),或者去掉內(nèi)層的括號(hào),都能讓代碼編譯通過:

std::array a1{{1,2}, {3,4}, {5,6}};// 原生數(shù)組的初始化寫法,編譯失敗!std::array a2{{{1,2}, {3,4}, {5,6}}};// 外層多一層括號(hào),編譯成功std::array a3{1,2,3,4,5,6};// 內(nèi)層不加括號(hào),編譯成功

這篇文章會(huì)介紹這個(gè)問題的原理,以及正確的解決方式。

聚合初始化

先從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成為一個(gè)聚合(aggregate)。

對(duì)聚合的定義,在每個(gè)C++版本中有少許的區(qū)別,這里簡(jiǎn)單總結(jié)下C++17中定義:一個(gè)class或struct類型,當(dāng)它滿足以下條件時(shí),稱為一個(gè)聚合[1]:

沒有private或protected數(shù)據(jù)成員;

沒有用戶提供的構(gòu)造函數(shù)(但是顯式使用=default或=delete聲明的構(gòu)造函數(shù)除外);

沒有virtual、private或者protected基類;

沒有虛函數(shù)

直觀的看,聚合常常對(duì)應(yīng)著只包含數(shù)據(jù)的struct類型,即常說的POD類型。另外,原生數(shù)組類型也都是聚合。

聚合初始化可以用大括號(hào)列表。一般大括號(hào)內(nèi)的元素與聚合的元素一一對(duì)應(yīng),并且大括號(hào)的嵌套也和聚合類型嵌套關(guān)系一致。在C語言中,我們常見到這樣的struct初始化語句。

解了上面的原理,就容易理解為什么std::array的初始化在多一層大括號(hào)時(shí)可以成功了——因?yàn)閟td::array內(nèi)部的唯一元素是一個(gè)原生數(shù)組,所以有兩層嵌套關(guān)系。下面展示一個(gè)自定義的MyArray類型,它的數(shù)據(jù)結(jié)構(gòu)和std::array幾乎一樣,初始化方法也類似:

struct S {

? ? int x;

? ? int y;

};

templatestruct MyArray {

? ? T data[N];

};int main()

{

? ? MyArray a1{{1,2,3}};// 兩層大括號(hào)MyArray a2{{{1,2}, {3,4}, {5,6}}};// 三層大括號(hào)return0;

}

在上面例子中,初始化列表的最外層大括號(hào)對(duì)應(yīng)著MyArray,之后一層的大括號(hào)對(duì)應(yīng)著數(shù)據(jù)成員data,再之后才是data中的元素。大括號(hào)的嵌套與類型間的嵌套完全一致。這才是std::array嚴(yán)格、完整的初始化大括號(hào)寫法。

可是,為什么當(dāng)std::array元素類型是簡(jiǎn)單類型時(shí),省掉一層大括號(hào)也沒問題?——這就涉及聚合初始化的另一個(gè)特點(diǎn):大括號(hào)省略。

大括號(hào)省略(brace elision)

C++允許在聚合的內(nèi)部成員仍然是聚合時(shí),省掉一層或多層大括號(hào)。當(dāng)有大括號(hào)被省略時(shí),編譯器會(huì)按照內(nèi)層聚合所含的元素個(gè)數(shù)進(jìn)行依次填充。

下面的代碼雖然不常見,但是是合法的。雖然二維數(shù)組初始化只用了一層大括號(hào),但因?yàn)榇罄ㄌ?hào)省略特性,編譯器會(huì)依次用所有元素填充內(nèi)層數(shù)組——上一個(gè)填滿后再填下一個(gè)。

inta[3][2]{1,2,3,4,5,6};// 等同于{{1, 2}, {3, 4}, {5, 6}}

知道了大括號(hào)省略后,就知道std::array初始化只用一層大括號(hào)的原理了:由于std::array的內(nèi)部成員數(shù)組是一個(gè)聚合,當(dāng)編譯器看到{1,2,3}這樣的列表時(shí),會(huì)挨個(gè)把大括號(hào)內(nèi)的元素填充給內(nèi)部數(shù)組的元素。甚至,假設(shè)std::array內(nèi)部有兩個(gè)數(shù)組的話,它還會(huì)在填完上一個(gè)數(shù)組后依次填下一個(gè)。

這也解釋了為什么省掉內(nèi)層大括號(hào),復(fù)雜類型也可以編譯成功:

std::array a3{1,2,3,4,5,6};// 內(nèi)層不加括號(hào),編譯成功

因?yàn)镾也是個(gè)聚合類型,所以這里省略了兩層大括號(hào)。編譯期按照下面的順序依次填充元素:數(shù)組0號(hào)元素的S::x、數(shù)組0號(hào)元素的S::y、數(shù)組1號(hào)元素的S::x、數(shù)組1號(hào)元素的S::y……

雖然大括號(hào)可以省略,但是一旦用戶顯式的寫出了大括號(hào),那么必須要和這一層的元素個(gè)數(shù)嚴(yán)格對(duì)應(yīng)。因此下面的寫法會(huì)報(bào)錯(cuò):

std::array a1{{1,2}, {3,4}, {5,6}};// 編譯失敗!

編譯器認(rèn)為{1,2}對(duì)應(yīng)std::array的內(nèi)部數(shù)組,然后{3,4}對(duì)應(yīng)std::array的下一個(gè)內(nèi)部成員。可是std::array只有一個(gè)數(shù)據(jù)成員,于是報(bào)錯(cuò):too many initializers for 'std::array<S, 3>'

需要注意的是,大括號(hào)省略只對(duì)聚合類型有效。如果S有個(gè)自定義的構(gòu)造函數(shù),省掉大括號(hào)就行不通了:

// 聚合struct S1 {

? ? S1() =default;

? ? int x;

? ? int y;

};

std::array a1{1,2,3,4,5,6};// OK// 聚合struct S2 {

? ? S2() = delete;

? ? int x;

? ? int y;

};

std::array a2{1,2,3,4,5,6};// OK// 非聚合,有用戶提供的構(gòu)造函數(shù)struct S3 {

? ? S3() {};

? ? int x;

? ? int y;

};

std::array a3{1,2,3,4,5,6};// 編譯失敗!

這里可以看出=default的構(gòu)造函數(shù)與空構(gòu)造函數(shù)的微妙區(qū)別。

std::initializer_list的另一個(gè)故事

上面講的所有規(guī)則,都只對(duì)聚合初始化有效。如果我們給MyArray類型加上一個(gè)接受std::initializer_list的構(gòu)造函數(shù),情況又不一樣了:

正在上傳...取消

struct S {

? ? int x;

? ? int y;

};

templatestruct MyArray {public:

? ? MyArray(std::initializer_list l)

? ? {

? ? ? ? std::copy(l.begin(), l.end(), std::begin(data));

? ? }

? ? T data[N];

};int main()

{

? ? MyArray a{{{1,2}, {3,4}, {5,6}}};// OKMyArray b{{1,2}, {3,4}, {5,6}};// 同樣OKreturn0;

}

正在上傳...取消

當(dāng)使用std::initializer_list的構(gòu)造函數(shù)來初始化時(shí),無論初始化列表外層是一層還是兩層大括號(hào),都能初始化成功,而且a和b的內(nèi)容完全一樣。

這又是為什么?難道std::initializer_list也支持大括號(hào)省略?

這里要提一件趣事:《Effective Modern C++》這本書在講解對(duì)象初始化方法時(shí),舉了這么一個(gè)例子[2]:

正在上傳...取消

class Widget {public:

? Widget();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // default ctorWidget(std::initializer_list 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的注釋卻是個(gè)技術(shù)錯(cuò)誤。這個(gè)w5的構(gòu)造函數(shù)調(diào)用時(shí)并非像w4那樣傳入一個(gè)空的std::initializer_list,而是傳入包含了一個(gè)元素的std::initializer_list。

即使像Scott Meyers這樣的C++大牛,都會(huì)在大括號(hào)的語義上搞錯(cuò),可見C++的相關(guān)規(guī)則充滿著陷阱!

連《Effective Modern C++》都弄錯(cuò)了的規(guī)則

幸好,《Effective Modern C++》作為一本經(jīng)典圖書,讀者眾多。很快就有讀者發(fā)現(xiàn)了這個(gè)錯(cuò)誤,之后Scott Meyers將這個(gè)錯(cuò)誤的闡述放在了書籍的勘誤表中[3]。

Scott Meyers還邀請(qǐng)讀者們和他一起研究正確的規(guī)則到底是什么,最后,他們把結(jié)論寫在了一篇文章里[4]。文章通過3種具有不同構(gòu)造函數(shù)的自定義類型,來揭示std::initializer_list匹配時(shí)的微妙差異。代碼如下:

正在上傳...取消

#include #include class DefCtor {

? int x;public:

? DefCtor(){}

};

class DeletedDefCtor {

? int x;public:

? DeletedDefCtor() = delete;

};

class NoDefCtor {

? int x;? ? public:

? NoDefCtor(int){}

};

templateclass X {public:

? X() { std::cout <<"Def Ctor\n"; }

? X(std::initializer_list il)

? {

? ? std::cout <<"il.size() = "<< il.size() <<'\n';

? }

};

int main()

{

? X a0({});// il.size = 0X b0{{}};// il.size = 1

? X a2({});// il.size = 0

? // X<DeletedDefCtor> b2{{}};? ? // error! attempt to use deleted constructor

? X a1({});// il.size = 0X b1{{}};// il.size = 0}

對(duì)于構(gòu)造函數(shù)已被刪除的非聚合類型,用{}初始化會(huì)觸發(fā)編譯錯(cuò)誤,因此b2的表現(xiàn)是容易理解的。但是b0和b1的區(qū)別就很奇怪了:一模一樣的初始化方法,為什么一個(gè)傳入std::initializer_list的長(zhǎng)度為1,另一個(gè)長(zhǎng)度為0?

構(gòu)造函數(shù)的兩步嘗試

問題的原因在于:當(dāng)使用大括號(hào)初始化來調(diào)用構(gòu)造函數(shù)時(shí),編譯器會(huì)進(jìn)行兩次嘗試:

把整個(gè)大括號(hào)列表連同最外層大括號(hào)一起,作為構(gòu)造函數(shù)的std::initializer_list參數(shù),看看能不能匹配成功;

如果第一步失敗了,則將大括號(hào)列表的成員作為構(gòu)造函數(shù)的入?yún)ⅲ纯茨懿荒芷ヅ涑晒Α?/p>

對(duì)于b0{{}}這樣的表達(dá)式,可以直觀理解第一步嘗試是:b0({{}}),也就是把{{}}整體作為一個(gè)參數(shù)傳給構(gòu)造函數(shù)。對(duì)b0來說,這個(gè)匹配是能夠成功的。因?yàn)镈efCtor可以通過{}初始化,所以b0的初始化調(diào)用了X(std::initializer_list<T>),并且傳入含有1個(gè)成員的std::initializer_list作為入?yún)ⅰ?/p>

對(duì)于b1{{}},編譯器同樣會(huì)先做第一步嘗試,但是NoDefCtor不允許用{}初始化,所以第一步嘗試會(huì)失敗。接下來編譯器做第二步嘗試,將外層大括號(hào)剝掉,調(diào)用b1({}),發(fā)現(xiàn)可以成功,這時(shí)傳入的是空的std::initializer_list。

再回頭看之前MyArray的例子,現(xiàn)在我們可以分析出兩種初始化分別是在哪一步成功的:

MyArray a{{{1,2}, {3,4}, {5,6}}};// 在第二步,剝掉外層大括號(hào)后匹配成功MyArray b{{1,2}, {3,4}, {5,6}};// 第一步整個(gè)大括號(hào)列表匹配成功

綜合小測(cè)試

到這里,大括號(hào)初始化在各種場(chǎng)景下的規(guī)則就都解析完了。不知道讀者是否徹底掌握了?

不妨來試一試下面的小測(cè)試:這段代碼里有一個(gè)僅含一個(gè)元素的std::array,其元素類型是std::tuple,tuple只有一個(gè)成員,是自定義類型S,S定義有默認(rèn)構(gòu)造函數(shù)和接受std::initializer_list<int>的構(gòu)造函數(shù)。對(duì)于這個(gè)類型,初始化時(shí)允許使用幾層大括號(hào)呢?下面的初始化語句有哪些可以成功?分別是為什么?

正在上傳...取消

struct S {

? ? S() =default;

? ? S(std::initializer_list) {}

};int main()

{

? ? usingMyType = std::array,1>;

? ? MyType a{};? ? ? ? ? ? // 1層MyType b{{}};// 2層MyType c{{{}}};// 3層MyType d{{{{}}}};// 4層MyType e{{{{{}}}}};// 5層MyType f{{{{{{}}}}}};// 6層MyType g{{{{{{{}}}}}}};// 7層return0;

}

USB Microphone https://www.soft-voice.com/

Wooden Speakers? https://www.zeshuiplatform.com/

亞馬遜測(cè)評(píng) www.yisuping.cn

深圳網(wǎng)站建設(shè)www.sz886.com

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,165評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,435評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,943評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,175評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,713評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,599評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,788評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,034評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評(píng)論 1 280
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,408評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,747評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容