有朋友在使用std::array時發(fā)現(xiàn)一個奇怪的問題:當元素類型是復合類型時,編譯通不過。
struct S {
? ? int x;
? ? int y;
};int main()
{
? ? inta1[3]{1,2,3};// 簡單類型,原生數(shù)組std::array a2{1,2,3};// 簡單類型,std::arrayS a3[3]{{1,2}, {3,4}, {5,6}};// 復合類型,原生數(shù)組std::array a4{{1,2}, {3,4}, {5,6}};// 復合類型巷蚪,std::array香拉,編譯失斃惭铩!return0;
}
按說std::array和原生數(shù)組的行為幾乎是一樣的凫碌,可為什么當元素類型不同時扑毡,初始化語法還會有差別?更蹊蹺的是盛险,如果多加一層括號瞄摊,或者去掉內(nèi)層的括號,都能讓代碼編譯通過:
std::array a1{{1,2}, {3,4}, {5,6}};// 原生數(shù)組的初始化寫法苦掘,編譯失敾恢摹!std::array a2{{{1,2}, {3,4}, {5,6}}};// 外層多一層括號鹤啡,編譯成功std::array a3{1,2,3,4,5,6};// 內(nèi)層不加括號惯驼,編譯成功
這篇文章會介紹這個問題的原理,以及正確的解決方式递瑰。
聚合初始化
先從std::array的內(nèi)部實現(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類型换淆,當它滿足以下條件時哗总,稱為一個聚合[1]:
沒有private或protected數(shù)據(jù)成員;
沒有用戶提供的構(gòu)造函數(shù)(但是顯式使用=default或=delete聲明的構(gòu)造函數(shù)除外)倍试;
沒有virtual讯屈、private或者protected基類;
沒有虛函數(shù)
直觀的看县习,聚合常常對應著只包含數(shù)據(jù)的struct類型涮母,即常說的POD類型。另外躁愿,原生數(shù)組類型也都是聚合叛本。
聚合初始化可以用大括號列表。一般大括號內(nèi)的元素與聚合的元素一一對應彤钟,并且大括號的嵌套也和聚合類型嵌套關(guān)系一致来候。在C語言中,我們常見到這樣的struct初始化語句逸雹。
解了上面的原理营搅,就容易理解為什么std::array的初始化在多一層大括號時可以成功了——因為std::array內(nèi)部的唯一元素是一個原生數(shù)組云挟,所以有兩層嵌套關(guān)系。下面展示一個自定義的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}};// 兩層大括號MyArray a2{{{1,2}, {3,4}, {5,6}}};// 三層大括號return0;
}
在上面例子中,初始化列表的最外層大括號對應著MyArray休蟹,之后一層的大括號對應著數(shù)據(jù)成員data沸枯,再之后才是data中的元素。大括號的嵌套與類型間的嵌套完全一致赂弓。這才是std::array嚴格辉饱、完整的初始化大括號寫法。
可是拣展,為什么當std::array元素類型是簡單類型時彭沼,省掉一層大括號也沒問題?——這就涉及聚合初始化的另一個特點:大括號省略备埃。
大括號省略(brace elision)
C++允許在聚合的內(nèi)部成員仍然是聚合時姓惑,省掉一層或多層大括號。當有大括號被省略時按脚,編譯器會按照內(nèi)層聚合所含的元素個數(shù)進行依次填充于毙。
下面的代碼雖然不常見,但是是合法的辅搬。雖然二維數(shù)組初始化只用了一層大括號唯沮,但因為大括號省略特性,編譯器會依次用所有元素填充內(nèi)層數(shù)組——上一個填滿后再填下一個堪遂。
inta[3][2]{1,2,3,4,5,6};// 等同于{{1, 2}, {3, 4}, {5, 6}}
知道了大括號省略后介蛉,就知道std::array初始化只用一層大括號的原理了:由于std::array的內(nèi)部成員數(shù)組是一個聚合,當編譯器看到{1,2,3}這樣的列表時溶褪,會挨個把大括號內(nèi)的元素填充給內(nèi)部數(shù)組的元素币旧。甚至,假設(shè)std::array內(nèi)部有兩個數(shù)組的話猿妈,它還會在填完上一個數(shù)組后依次填下一個吹菱。
這也解釋了為什么省掉內(nèi)層大括號,復雜類型也可以編譯成功:
std::array a3{1,2,3,4,5,6};// 內(nèi)層不加括號彭则,編譯成功
因為S也是個聚合類型鳍刷,所以這里省略了兩層大括號。編譯期按照下面的順序依次填充元素:數(shù)組0號元素的S::x俯抖、數(shù)組0號元素的S::y输瓜、數(shù)組1號元素的S::x、數(shù)組1號元素的S::y……
雖然大括號可以省略,但是一旦用戶顯式的寫出了大括號前痘,那么必須要和這一層的元素個數(shù)嚴格對應凛捏。因此下面的寫法會報錯:
std::array a1{{1,2}, {3,4}, {5,6}};// 編譯失敗芹缔!
編譯器認為{1,2}對應std::array的內(nèi)部數(shù)組坯癣,然后{3,4}對應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 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的另一個故事
上面講的所有規(guī)則迟赃,都只對聚合初始化有效陪拘。如果我們給MyArray類型加上一個接受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;
}
正在上傳...取消
當使用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 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的注釋卻是個技術(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 #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}
對于構(gòu)造函數(shù)已被刪除的非聚合類型萍倡,用{}初始化會觸發(fā)編譯錯誤,因此b2的表現(xiàn)是容易理解的辟汰。但是b0和b1的區(qū)別就很奇怪了:一模一樣的初始化方法列敲,為什么一個傳入std::initializer_list的長度為1,另一個長度為0帖汞?
構(gòu)造函數(shù)的兩步嘗試
問題的原因在于:當使用大括號初始化來調(diào)用構(gòu)造函數(shù)時戴而,編譯器會進行兩次嘗試:
把整個大括號列表連同最外層大括號一起,作為構(gòu)造函數(shù)的std::initializer_list參數(shù)翩蘸,看看能不能匹配成功所意;
如果第一步失敗了,則將大括號列表的成員作為構(gòu)造函數(shù)的入?yún)⒋呤祝纯茨懿荒芷ヅ涑晒Α?/p>
對于b0{{}}這樣的表達式扁眯,可以直觀理解第一步嘗試是:b0({{}}),也就是把{{}}整體作為一個參數(shù)傳給構(gòu)造函數(shù)翅帜。對b0來說姻檀,這個匹配是能夠成功的。因為DefCtor可以通過{}初始化涝滴,所以b0的初始化調(diào)用了X(std::initializer_list<T>)绣版,并且傳入含有1個成員的std::initializer_list作為入?yún)ⅰ?/p>
對于b1{{}},編譯器同樣會先做第一步嘗試歼疮,但是NoDefCtor不允許用{}初始化杂抽,所以第一步嘗試會失敗。接下來編譯器做第二步嘗試韩脏,將外層大括號剝掉缩麸,調(diào)用b1({}),發(fā)現(xiàn)可以成功赡矢,這時傳入的是空的std::initializer_list杭朱。
再回頭看之前MyArray的例子,現(xiàn)在我們可以分析出兩種初始化分別是在哪一步成功的:
MyArray a{{{1,2}, {3,4}, {5,6}}};// 在第二步吹散,剝掉外層大括號后匹配成功MyArray b{{1,2}, {3,4}, {5,6}};// 第一步整個大括號列表匹配成功
綜合小測試
到這里弧械,大括號初始化在各種場景下的規(guī)則就都解析完了。不知道讀者是否徹底掌握了空民?
不妨來試一試下面的小測試:這段代碼里有一個僅含一個元素的std::array屋摇,其元素類型是std::tuple,tuple只有一個成員号醉,是自定義類型S,S定義有默認構(gòu)造函數(shù)和接受std::initializer_list<int>的構(gòu)造函數(shù)衔瓮。對于這個類型,初始化時允許使用幾層大括號呢抖甘?下面的初始化語句有哪些可以成功热鞍?分別是為什么?
正在上傳...取消
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/
亞馬遜測評 www.yisuping.cn
深圳網(wǎng)站建設(shè)www.sz886.com