要是世上未曾存在C++14和C++17該有多好!constexpr
是好東西,可是讓編譯器開發者痛不欲生;新標準庫的確好用,但改語法細節未必是明智之舉,尤爲是3年一次的頻繁改動。C++帶了太多歷史包袱,咱們都是爲之買帳的一員。html
我沒那麼多精力考慮C++14/17的問題,因此本文基於C++11標準。ios
知其因此然,是學習C++愈加複雜的語法的最佳方式。所以,咱們從列表初始化的動機講起。數組
早在2005年,Bjarne Stroustrup就提出要統一C++中的初始化語法。這是由於在C++11之前,初始化存在一系列問題,包括:函數
4種初始化方式:X t1 = v;
、X t2(v);
、X t3 = { v };
、X t4 = X(v);
;工具
聚合(aggregate)初始化;學習
default
與explicit
;測試
……代理
雖然每個都有辦法解決,但加在一塊兒將會變得很是複雜,對編譯器和開發者都是負擔。換句話說,惟一的需求就是一種統一的初始化語法,其適用範圍能涵蓋先前的各類問題。code
因而,列表初始化誕生了。htm
正由於列表初始化是爲解決初始化問題而生,列表初始化的適用範圍是任何初始化。你能想到的都寫寫看,寫對就是賺到。
固然,全憑感受是行不通的,仍是得講點道理。列表初始化分爲兩類:直接初始化與拷貝初始化。
在直接初始化中,不管構造函數是否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 <iostream> #include <utility> 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 }); }
列表不是表達式,更不屬於任何類型,因此decltype({1, 2})
是非法的,這還適用於模板參數推導。可是在如下幾種狀況中,列表能夠轉換成std::initializer_list<T>
實例:
直接初始化中,對應構造函數參數類型爲std::initializer_list<T>
;
拷貝初始化中,對應參數類型爲std::initializer_list<T>
;
綁定到auto
上(列表元素類型必須嚴格一致),包括範圍for
(range for)循環——當綁定auto&&
時,變量的實際類型爲std::initializer_list<T>&&
,這是轉發引用的特例。
std::initializer_list
是爲列表初始化提供的特殊的工具,是一個輕量級的數組代理(proxy),其元素類型爲const T
。雖然你能在<initializer_list>
中看到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
有更高的優先級。