溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

如何理解C++的列表初始化語(yǔ)法

發(fā)布時(shí)間:2021-10-18 10:26:27 來(lái)源:億速云 閱讀:142 作者:柒染 欄目:編程語(yǔ)言

如何理解C++的列表初始化語(yǔ)法,相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。

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

struct S {
    int x;
    int y;
};

int main()
{
    int a1[3]{1, 2, 3};  // 簡(jiǎn)單類(lèi)型,原生數(shù)組
    std::array<int, 3> a2{1, 2, 3};  // 簡(jiǎn)單類(lèi)型,std::array
    S a3[3]{{1, 2}, {3, 4}, {5, 6}};  // 復(fù)合類(lèi)型,原生數(shù)組
    std::array<S, 3> a4{{1, 2}, {3, 4}, {5, 6}};  // 復(fù)合類(lèi)型,std::array,編譯失??!
    return 0;
}

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

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

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

聚合初始化

先從std::array的內(nèi)部實(shí)現(xiàn)說(shuō)起。為了讓std::array表現(xiàn)得像原生數(shù)組,C++中的std::array與其他STL容器有很大區(qū)別——std::array沒(méi)有定義任何構(gòu)造函數(shù),而且所有內(nèi)部數(shù)據(jù)成員都是public的。這使得std::array成為一個(gè)聚合aggregate)。

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

  1. 沒(méi)有privateprotected數(shù)據(jù)成員;

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

  3. 沒(méi)有virtual、private或者protected基類(lèi);

  4. 沒(méi)有虛函數(shù)

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

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

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

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}};  // 兩層大括號(hào)
    MyArray<S, 3> a2{{{1, 2}, {3, 4}, {5, 6}}};  // 三層大括號(hào)
    return 0;
}

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

可是,為什么當(dāng)std::array元素類(lèi)型是簡(jiǎn)單類(lèi)型時(shí),省掉一層大括號(hào)也沒(méi)問(wèn)題?——這就涉及聚合初始化的另一個(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)行依次填充。

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

int a[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ù)雜類(lèi)型也可以編譯成功:

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

因?yàn)?strong>S也是個(gè)聚合類(lèi)型,所以這里省略了兩層大括號(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ù)顯式的寫(xiě)出了大括號(hào),那么必須要和這一層的元素個(gè)數(shù)嚴(yán)格對(duì)應(yīng)。因此下面的寫(xiě)法會(huì)報(bào)錯(cuò):

std::array<S, 3> a1{{1, 2}, {3, 4}, {5, 6}};  // 編譯失?。?/pre>

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

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

// 聚合
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

// 非聚合,有用戶(hù)提供的構(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的另一個(gè)故事

上面講的所有規(guī)則,都只對(duì)聚合初始化有效。如果我們給MyArray類(lèi)型加上一個(gè)接受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}}};  // OK
    MyArray<S, 3> b{{1, 2}, {3, 4}, {5, 6}};  // 同樣OK
    return 0;
}

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

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

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

class Widget {
public:
  Widget();                                   // default ctor
  Widget(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 <-注意!

然而,書(shū)里這段代碼最后一行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)的語(yǔ)義上搞錯(cuò),可見(jiàn)C++的相關(guān)規(guī)則充滿(mǎn)著陷阱!

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

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

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

#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 Ctor\n"; }
 
  X(std::initializer_list<T> il)
  {
    std::cout << "il.size() = " << il.size() << '\n';
  }
};
 
int main()
{
  X<DefCtor> a0({});           // il.size = 0
  X<DefCtor> b0{{}};           // il.size = 1
 
  X<DeletedDefCtor> a2({});    // il.size = 0
  // X<DeletedDefCtor> b2{{}};    // error! attempt to use deleted constructor
 
  X<NoDefCtor> a1({});         // il.size = 0
  X<NoDefCtor> b1{{}};         // il.size = 0
}

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

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

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

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

  2. 如果第一步失敗了,則將大括號(hào)列表的成員作為構(gòu)造函數(shù)的入?yún)?,看看能不能匹配成功?/p>

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

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

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

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

綜合小測(cè)試

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

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

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;
}

看完上述內(nèi)容,你們掌握如何理解C++的列表初始化語(yǔ)法的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI