C++統一初始化語法(列表初始化)

C++統一初始化語法(列表初始化)

引言
要是世上不曾存在C++14和C++17該有多好!constexpr是好東西,但是讓編譯器開發者痛不欲生;新標準庫的確好用,但改語法細節未必是明智之舉,尤其是3年一次的頻繁改動。C++帶了太多歷史包袱,我們都是爲之買賬的一員。

我沒那麼多精力考慮C++14/17的問題,所以本文基於C++11標準。

知其所以然,是學習C++越發複雜的語法的最佳方式。因此,我們從列表初始化的動機講起。

動機
早在2005年,Bjarne Stroustrup就提出要統一C++中的初始化語法。這是因爲在C++11以前,初始化存在一系列問題,包括:

4種初始化方式:X t1 = v;、X t2(v);、X t3 = { v };、X t4 = X(v);;
聚合(aggregate)初始化;
default與explicit;
……
雖然每一個都有辦法解決,但加在一起將會變得非常複雜,對編譯器和開發者都是負擔。換句話說,唯一的需求就是一種統一的初始化語法,其適用範圍能涵蓋先前的各種問題。

於是,列表初始化誕生了。

語法
正因爲列表初始化是爲解決初始化問題而生,列表初始化的適用範圍是任何初始化。你能想到的都寫寫看,寫對就是賺到。

當然,全憑感覺是行不通的,還是得講點道理。列表初始化分爲兩類:直接初始化與拷貝初始化。

在直接初始化中,無論構造函數是否explicit,都有可能被調用:

T object { arg1, arg2, ... };,用arg1, arg2, ...構造T類型的對象object——參數可以是一個值,也可以是一個初始化列表,下同;
Class { T member { arg1, arg2, ... }; };,構造member成員對象——花括號的優勢在這裏體現出來,因爲如果是圓括號的話member會被看作一個函數;
T { arg1, arg2, ... },構造臨時對象;
new T { arg1, arg2, ... },構造heap上的對象;
Class::Class() : member{arg1, arg2, ...} {...,成員初始化列表——除了2以外,其餘都與用()初始化沒有區別。
在拷貝初始化中,無論構造函數是否explicit都會被考慮,但是如果重載決議爲一個explicit函數,則此調用錯誤:

T object = {arg1, arg2, ...};,與直接初始化中的1類似,除了explicit以外都相同,operator=不會被調用;
object = { arg1, arg2, ... },賦值語句,調用operator=;
Class { T member = { arg1, arg2, ... }; };,與直接初始化中的2類似,explicit同理;
function( { arg1, arg2, ... } ),構造函數參數;
return { arg1, arg2, ... } ;,構造返回值;
object[ { arg1, arg2, ... } ],構造operator[]的參數;
U( { arg1, arg2, ... } ),構造U構造函數的參數。
4~7可以概括爲,在該有一個對象的地方,可以用一個列表來構造它。這句話不是很嚴謹,因爲除了operator()和operator[]以外,其他運算符的參數都不能用列表初始化。

還有一個要注意的地方,是列表初始化不允許窄化轉換(narrowing conversion),即可能丟失信息的轉換,如float轉換爲int。

include

include

struct Test
{

Test(int, int)
{
    std::cout << "Test(int, int)" << std::endl;
}
explicit Test(int, int, int)
{
    std::cout << "explicit Test(int, int, int)" << std::endl;
}
void operator[](std::pair<int, int>)
{
    std::cout << "void operator[](std::pair<int, int>)" << std::endl;
}
void operator()(std::pair<int, int>)
{
    std::cout << "void operator()(std::pair<int, int>)" << std::endl;
}

};

Test test()
{

return { 1, 2 };

}

int main()
{

Test t{ 1, 2 };
Test t1 = { 1, 2 };
Test t2 = { 1, 2, 3 }; // error
t[{ 1, 2 }];
t({ 1, 2 });

}

initializer_list
列表不是表達式,更不屬於任何類型,所以decltype({1, 2})是非法的,這還適用於模板參數推導。但是在以下幾種情況中,列表可以轉換成std::initializer_list實例:

直接初始化中,對應構造函數參數類型爲std::initializer_list;
拷貝初始化中,對應參數類型爲std::initializer_list;
綁定到auto上(列表元素類型必須嚴格一致),包括範圍for(range for)循環——當綁定auto&&時,變量的實際類型爲std::initializer_list&&,這是轉發引用的特例。
std::initializer_list是爲列表初始化提供的特殊的工具,是一個輕量級的數組代理(proxy),其元素類型爲const T。雖然你能在中看到std::initializer_list類模板的實現,但它實際上是與編譯器內部綁定的,你無法用一個自己寫的相似的類替換它(除非改編譯器)。

std::initializer_list有構造函數、size、begin和end函數,用法與其他STL順序容器類似。迭代器解引用得到const T&類型,元素是不能修改的。

std::initializer_list帶來的最明顯的進步就是STL容器可以用列表來初始化,無需再寫那麼多push_back了。

重載決議
struct Test
{

Test(int, int)
{
    std::cout << "Test(int, int)" << std::endl;
}
Test(std::initializer_list<int>)
{
    std::cout << "Test(std::initializer_list<int>)" << std::endl;
}

};
如果我寫Test{1, 2},哪個構造函數會被調用呢?回答這個問題,需要對與列表相關的重載決議有所瞭解。

對於涉及到構造函數的列表初始化(不涉及到的包括聚合初始化等),各構造函數分兩個階段考慮:

如果有構造函數第一個參數爲std::initializer_list,沒有其他參數或其他參數都有默認值,則匹配該構造函數(這裏似乎允許窄化轉換,我測試起來也是如此)——std::initializer_list優先級高;
否則,所有構造函數參與重載決議,除了窄化轉換不允許,以及拷貝初始化與explicit的衝突依然有效。
所以上面那段程序中Test{1, 2}會匹配第二個構造函數。

如果有多個std::initializer_list重載呢?衆所周知,重載決議中參數轉換有完美、提升、轉換三個等級,std::initializer_list參數的轉換等級定義爲所有元素中最差的(不允許窄化轉換),然後找出等級最高的調用,如果有多個則爲二義調用。

如果沒有std::initializer_list重載呢?由於從列表到參數本身就是轉換,屬於最差的等級,如果有多個函數可以通過參數轉換後匹配,則該調用就是二義調用;只有當只有一個函數可行時才合法。

總結
列表初始化是一種萬能的初始化語法,適用範圍廣導致其規則比較複雜,我們應當結合其動機來理解標準規定的行爲。

列表初始化包括直接初始化與拷貝初始化,後者涵蓋了參數與返回值等情形。當我們不想要隱式拷貝初始化時,要用explicit關鍵字來拒絕。

列表不屬於任何類型,但一些情況下可以轉換成std::initializer_list。在重載決議中,std::initializer_list有更高的優先級。

原文地址https://www.cnblogs.com/jerry-fuyi/p/12806284.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章