C++ 列表初始化
C++11 中,引入了用 {}
執行初始化的統一形式,C++11 稱這種形式為統一初始化(Uniform initialization)
使用這種形式,可以解決所謂的“C++最令人惱怒的解析問題”
最令人惱怒的解析問題
該問題源於函數風格的轉型和函數聲明之間的相似性,導致很多代碼都會被看做是函數聲明
考慮這段代碼
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
原意是用一對流迭代器去初始化列表,但運行后你會發現,它什么都沒有做
原因在於編譯器會認為 data 是一個函數
- 第一個參數 dataFile 是
istream_iterator<int>
類型的對象 - 第二個參數是不接受參數,返回
istream_iterator<int>
對象的函數
因為在函數聲明中
- 包裹形參名的圓括號會被無視
int f(double d);
和int f(double (d));
是等價的- 參數是 double 類型的 d
- 空的圓括號會被認為是這里有一個函數類型的參數
int g(double ());
- 參數是返回值為 double,參數為空的函數
class Widget
{
//假設Widget有默認構造器
};
Widget w();
在這個例子中,w 並不是 Widget 對象,而是返回值為 Widget 類型的函數
C++ 11 前的解決方案
將參數聲明包裹到圓括號里是非法的,但將函數調用的參數包裹在圓括號里是合法的:
list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());
一種可讀性更好、更合理的方式:
ifstream dataFile("ints.dat");
istream_iterator<int> begin(dataFile);
istream_iterator<int> end;
list<int> data(begin, end);
列表初始化
用列表初始化來解決上面的問題
list<int> data{ istream_iterator<int>{dataFile}, istream_iterator<int>{} };
Obj(1.0)
和 Obj{1.0}
的一個區別是:前者允許調用 Obj(int)
,而列表初始化不允許存在精度損失的類型轉換
列表初始化的限制
列表初始化主要的限制是,如果一個類既有使用初始化列表的構造函數,又有不使用初始化列表的構造函數,那編譯器會千方百計地試圖調用使用初始化列表的構造函數,導致各種意外
vector<int> v{3, -1};
for (auto i : v) {
cout << i << endl;
}
比如上面的代碼,可能本意是構造一個 3 個元素,初始值都是 -1 的 vector,但編譯器會去嘗試調用列表初始化構造函數,構造一個 [3, -1]
vector 出來
對此,比較好的做法是:
- 如果一個類沒有使用初始化列表的構造函數時,初始化該類對象可全部使用統一初始化語法
- 如果一個類有使用初始化列表的構造函數時,則只應用在初始化列表構造的情況
initializer_list
initializer_list
定義在同名頭文件中
template< class T >
class initializer_list;
initializer_list
是一種輕量級的代理對象,用來訪問 T 類型的對象數組
- 可以實現為一對指針,或指針加數組長度
- 當
initializer_list
對象被拷貝時,底層數組不會被拷貝 - 底層數組是
const T[N]
類型的臨時數組- 每個元素都是從
{}
列表中拷貝來的 initializer_list
對象中的元素永遠是常量值,無法被修改
- 每個元素都是從
- 有點像
string_view
或 asio 中的 buffer
在以下幾種情況中, initializer_list
對象會被自動構造
- 用
{}
列表初始化對象,且該對象的構造函數接受initializer_list
參數- 標准庫容器基本都會接受
initializer_list<value_type>
類型的參數 - 因此標准庫容器基本都是可以使用列表初始化的
- 標准庫容器基本都會接受
- 用
{}
列表作為賦值語句的右操作數/函數的參數,對應的賦值運算符/函數接受initializer_list
參數