天天看點

C++統一初始化文法(清單初始化)include include

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

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了。

重載決議

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

繼續閱讀