C++中的initializer_list
C++中的initializer_list
- 型別定義
- 原始碼
- typedef
- 私有成員變數
- constructor
- backing array的生命週期
- 函數g中的支援陣列__a
- 函數h中的支援陣列__b
- size
- begin/end
- 使用例子
- 傳遞給函式
- 用來初始化容器
- 用於ranged for loop
型別定義
std::initializer_list
std::initializer_listC++ Utilities library std::initializer_list
(not to be confused with member initializer list)Defined in header <initializer_list>
template< class T >
class initializer_list;
(since C++11)
An object of type std::initializer_list<T> is a lightweight proxy object that provides access to an array of objects of type const T (that may be allocated in read-only memory).A std::initializer_list object is automatically constructed when:a brace-enclosed initializer list is used to list-initialize an object, where the corresponding constructor accepts an std::initializer_list parameter,
a brace-enclosed initializer list is used as the right operand of assignment or as a function call argument, and the corresponding assignment operator/function accepts an std::initializer_list parameter,
a brace-enclosed initializer list is bound to auto, including in a ranged for loop.
std::initializer_list may be implemented as a pair of pointers or pointer and length. Copying a std::initializer_list does not copy the backing array of the corresponding initializer list.The program is ill-formed if an explicit or partial specialization of std::initializer_list is declared.
C++ 公用程式庫中的 std::initializer_list
(注意不要與「成員初始化列表」(member initializer list)混淆)
定義於標頭檔 <initializer_list>
template< class T >
class initializer_list;
(自 C++11 起)
型別為 std::initializer_list<T>
的物件是一個輕量級代理物件(lightweight proxy object),可以用來存取 const T
陣列(這個陣列可能配置在唯讀記憶體中)。
std::initializer_list
物件會在下列情況中自動建構:
- 情況一:當使用大括號包住的初始化列表(brace-enclosed initializer list)來列表初始化(list-initialize) 物件,且對應的constructor接受
std::initializer_list
參數時。 - 情況二:當使用大括號包住的初始化列表被當作 賦值運算子的右運算元 或 函式呼叫的引數,且對應的賦值運算子/函式接受
std::initializer_list
參數時。 - 情況三:當大括號包住的初始化列表被 綁定給 auto(bound to auto)時。ranged for loop也包含在這種情況內。
編譯器可能以「一對指標」或「指標和長度」來實作std::initializer_list
。
複製一個 std::initializer_list
並不會複製其對應的初始化列表的底層陣列。
如果程式中顯示(完全)或部分特化(explicit or partial specialization)std::initializer_list
,則該程式將屬於ill-formed,無法編譯。
原始碼
GCC編譯器對initializer_list
的實作位於libstdc++ - initializer_list:
45 /// initializer_list46 template<class _E>47 class initializer_list48 {49 public:50 typedef _E value_type;51 typedef const _E& reference;52 typedef const _E& const_reference;53 typedef size_t size_type;54 typedef const _E* iterator;55 typedef const _E* const_iterator;56 57 private:58 iterator _M_array;59 size_type _M_len;60 61 // The compiler can call a private constructor.62 constexpr initializer_list(const_iterator __a, size_type __l)63 : _M_array(__a), _M_len(__l) { }64 65 public:66 constexpr initializer_list() noexcept67 : _M_array(0), _M_len(0) { }68 69 // Number of elements.70 constexpr size_type71 size() const noexcept { return _M_len; }72 73 // First element.74 constexpr const_iterator75 begin() const noexcept { return _M_array; }76 77 // One past the last element.78 constexpr const_iterator79 end() const noexcept { return begin() + size(); }80 };
這段程式碼可分為以下幾個部分:
- 使用
typedef
定義成員型別 - 私有成員變數
- 兩個constructor
size
begin
和end
注意initializer_list
並沒有負責元素複製或內存管理的函數。
typedef
std::initializer_list文檔中將各typedef
整理如下:
成員型別 | 定義 | 說明 |
---|---|---|
value_type | T | 元素的型別 |
reference | const T& | 取元素時得到的 reference(const) |
const_reference | const T& | 同上 |
size_type | std::size_t | 元素個數 |
iterator | const T* | 遍歷元素的指標(const) |
const_iterator | const T* | 同上 |
私有成員變數
私有成員變數只有:iterator _M_array
和size_type _M_len
,所以 initializer_list
其實只是包了一個指向常量陣列的指標跟一個長度值的類別。因為該陣列是常量陣列,所以我們無法修改其內容。
constructor
initializer_list
有兩個constructor,其中public版本不接受任何參數;而另一個接受指標和長度的constructor則是private的,只有編譯器可以用。
關於initializer_list
的建構,List-initialization (since C++11)提到:
An object of type std::initializer_list<E> is constructed from an initializer list as if the compiler generated and materialized(since C++17) a prvalue of type “array of N const E”, where N is the number of initializer clauses in the initializer list; this is called the initializer list’s backing array.Each element of the backing array is copy-initialized with the corresponding initializer clause of the initializer list, and the std::initializer_list<E> object is constructed to refer to that array. A constructor or conversion function selected for the copy is required to be accessible in the context of the initializer list. If a narrowing conversion is required to initialize any of the elements, the program is ill-formed.
一個型別為 std::initializer_list<E>
的物件,是從初始化列表(initializer list)建構出來的,就好像編譯器生成並實體化(materialized)(自 C++17 起)一個型別為「大小為 N 的 const E
陣列」的 prvalue,其中 N 是初始化列表中初始化子句(initializer clauses)的數量;這個陣列被稱為該初始化列表的 支援陣列(backing array)。
支援陣列中的每個元素,都是由初始化列表中對應的初始化子句進行複製初始化(copy-initialize) 而來的,而 std::initializer_list<E>
物件則被建構來引用該支援陣列。
用於複製初始化的建構函式或轉換函式(conversion function)必須在初始化列表的上下文(context)中可被存取。若初始化任何元素時需要做窄化轉換(narrowing conversion),則程式為不合法(ill-formed)。
注1:prvalue包括基本字面值(literals), 返回非引用類型的函數(function calls that return a nonreference type)和臨時物件(temporary objects),詳見Lvalues and Rvalues (C++)
注2:initializer clause即初始化列表中的元素,可以是expression, {}
或 { initializer-list }
,參考C++ - Initialization。
注3:根據Copy-initialization,如果初始值是右值(rvalue),則多載決議(overload resolution)會選擇move constructor。即便如此,這仍然算作「複製初始化」(copy-initialization);C++ 中沒有「移動初始化」(move-initialization)這樣的專門術語。
List-initialization (since C++11)中的例子:
void f(std::initializer_list<double> il);void g(float x)
{f({1, x, 3});
}void h()
{f({1, 2, 3});
}struct A { mutable int i; };void q(std::initializer_list<A>);void r()
{q({A{1}, A{2}, A{3}});
}
對於以上的初始化程式,編譯器會以大致等同於以下的方式來實作:
// The initialization above will be implemented in a way roughly equivalent to below,
// assuming that the compiler can construct an initializer_list object with a pair of
// pointers, and with the understanding that `__b` does not outlive the call to `f`.void g(float x)
{const double __a[3] = {double{1}, double{x}, double{3}}; // backing arrayf(std::initializer_list<double>(__a, __a + 3));
}void h()
{static constexpr double __b[3] ={double{1}, double{2}, double{3}}; // backing arrayf(std::initializer_list<double>(__b, __b + 3));
}void r()
{const A __c[3] = {A{1}, A{2}, A{3}}; // backing arrayq(std::initializer_list<A>(__c, __c + 3));
}
f
, g
和h
三個函數都可以拆成以下幾個步驟:
- 生成一個暫時的常量陣列(backing array)
- 假設編譯器能夠用一對指標來建構一個
initializer_list
物件,程式會使用該隱藏建構子建構出initializer_list
:成員變數const E* _M_array
會被設為陣列地址,成員變數size_t _M_len
被設為陣列長度 - 將
initializer_list
物件傳入函數f
或q
以上程式碼展示了backing array的產生 以及 initializer_list
是如何指向它的。
backing array的生命週期
List-initialization (since C++11)中還寫道:
The backing array has the same lifetime as any other temporary object, except that initializing an std::initializer_list object from the backing array extends the lifetime of the array exactly like binding a reference to a temporary.
這個支援陣列的生命週期與其他暫時物件相同,但若用該支援陣列初始化一個 std::initializer_list
物件,則會延長支援陣列的生命週期,就像將一個暫時物件綁定到參考一樣。
我們從上面的例子中來看看各backing array的生命週期:
函數g中的支援陣列__a
void g(float x)
{f({1, x, 3});
}
等同於:
void g(float x)
{const double __a[3] = {double{1}, double{x}, double{3}}; // backing arrayf(std::initializer_list<double>(__a, __a + 3));
}
因為std::initializer_list
只是持有指向 __a
的指標,沒有複製陣列,因此必須保證 __a
的生命週期覆蓋 f()
的執行。
關於支援陣列__a
,我們可以從程式碼中知道以下幾點:
- 儲存方式:automatic storage,位於 stack 上
- 生命週期:從宣告開始,到
g()
函式作用域結束時銷毀,與此同時std::initializer_list
內的指標失效
因為f()
函數返回後,g()
函數的作用域才會結束,所以以上這段程式碼是安全的。但如果我們把 std::initializer_list
保存起來,在 g()
回傳後使用,就會產生懸垂引用(dangling pointer)的問題。詳見What is a dangling pointer?
注:關於automatic storage和接下來會看到的static storage,詳見Storage Class,Storage class specifiers和Why are the terms “automatic” and “dynamic” preferred over the terms “stack” and “heap” in C++ memory management?。
函數h中的支援陣列__b
void h()
{f({1, 2, 3});
}
void h()
{static constexpr double __b[3] ={double{1}, double{2}, double{3}}; // backing arrayf(std::initializer_list<double>(__b, __b + 3));
}
關於支援陣列__b
,我們可以從程式碼中知道以下幾點:
- 儲存方式:static storage,位於static data region
- 生命週期:從程式啟動時建立,到程式結束才銷毀
因為__b
是 static constexpr
,所以跟前一個例子不同, __b
的位址永遠有效,不會懸垂。即便 initializer_list
被傳出去並在函式h
外使用,也依然安全,因為 backing array 永遠存在。
範例的注釋中提到:
with the understanding that __b does not outlive the call to f.
翻譯如下:需理解 __b
的生命週期不會比對 f
的呼叫更長。
但是這句話和 static constexpr double __b[3]
的真實生命週期矛盾,因為 static
變數實際上是與程式本身共存亡的。
其實這裡的註解不是在講 b
的實際生命週期,而是說:這個範例只是示意,編譯器生成的 backing array 的生命週期只需要保證能覆蓋 f()
的執行,不需要比它更長。
也就是說對於像 h()
這種情況,編譯器可以選擇把 backing array 做成靜態儲存(就像示例裡的 static constexpr
),也就是std::initializer_list中所說的:
that may be allocated in read-only memory
編譯器也可以選擇將backing array放在暫時的區域,只要保證呼叫 f()
的期間它還活著就好。
does not outlive the call to f
這個註解是提醒 programmer不要期待這個 backing array 的位址在 h()
返回後仍然存在(即便實作中可能真的把它放在靜態記憶體內)。
size
回傳成員變數_M_len
。
begin/end
begin()
和end()
返回的是const_iterator
,即const T*
。
使用例子
傳遞給函式
#include <iostream>void foo(std::initializer_list<int> il) {std::cout << il.size() << std::endl;
}int main() {// 編譯器生成 static const int[3],然後建構 initializer_listfoo({10, 20, 30});return 0;
}
因為函數foo
接受initializer_list
為參數,符合initializer_list
自動建構的情況二,所以此處會由brace-enclosed initializer list自動建構出一個initializer_list
後,將它當作參數傳給foo
。
foo
函數中接著調用initializer_list
的size
函數獲取其長度。
用來初始化容器
#include <iostream>
#include <vector>class Animal {
public:Animal(int f, bool t) : feet(f), tail(t) {}Animal(const Animal& a) : feet(a.feet), tail(a.tail) {std::cout << "copy construct " << a.feet << ", " << a.tail << std::endl;}Animal(Animal&& a) {std::cout << "move construct " << a.feet << ", " << a.tail << std::endl;feet = a.feet;tail = a.tail;a.feet = 0;a.tail = false;}private:int feet;bool tail;
};int main() {Animal a1 = { 4, true };Animal a2 = {2, false};std::vector<Animal> vec_animal = { a1, std::move(a2) };return 0;
}
copy construct 4, 1
move construct 2, 0
copy construct 4, 1
copy construct 2, 0
因為vector
有個接受initializer_list
的建構子,符合initializer_list
自動建構的情況一,所以此處會由brace-enclosed initializer list自動建構出一個initializer_list
,再將它當作參數傳給vector
的建構子。
從執行結果中可以看到在建構vec_animal
時共呼叫了4次constructor。前兩次是由大括號裡的元素構建initializer_list
,後兩次則是由intializer_list
初始化vector
。
大括號裡的第二個元素是std::move(a2)
,因此構建initializer_list
時便呼叫了Animal
的move consttructor;那麼為何在構建vector
時卻沒有用move constructor而用了copy constructor呢?
這其實跟std::initializer_list<T>
的迭代器有關,因為迭代器是const T*
型別的,所以vector
的建構子在從迭代器中拿元素時只能拿到 const T&
型別的物件。
而move constructor只接受非 const 的右值引用T&&
(可以被修改)。const T&
為常量,不能綁定到需要修改的物件,因此也就無法被轉換成T&&
,所以此處才無法調用 move constructor,只能退而求其次地調用copy constructor。
如果想真正 move 進 vector
,得避開 initializer_list
這個「陷阱」,改寫成:
std::vector<Animal> v;
v.push_back(std::move(a1));
v.push_back(std::move(a2));
這樣才會直接調用 move constructor。
用於ranged for loop
#include <iostream>int main() {for (int v : {10, 20, 30}) std::cout << v << ' ';return 0;
}
在ranged for loop中使用brace-enclosed initializer list符合initializer_list
自動建構的情況三,所以此處會由brace-enclosed initializer list自動建構出一個initializer_list
讓for loop來遍歷。
for loop中v
的型別是為int
,如果換成const int&
仍然可以編譯通過,但如果換成int&
,就會出現以下編譯錯誤:
main.cpp: In function ‘void foo(std::initializer_list<int>)’:
main.cpp:13:19: error: binding reference of type ‘int&’ to ‘const int’ discards qualifiers13 | for (int& v : il) std::cout << v << ' ';| ^~
這是因為遍歷initializer_list
時,獲取到的元素型別為const int &
,而const int &
型別的變數顯然是無法跟int &
綁定的。