C++中的List-initialization
C++中的List-initialization
List-initialization
官方文檔對List-initialization的介紹如下:
定義
List-initialization (since C++11)
C++ C++ language Initialization
Initializes an object from a brace-enclosed initializer list.
列表初始化(List-initialization)(自C++11 起)
用「大括號括起來的初始化列表」(brace-enclosed initializer list)來初始化一個物件。
語法
直接列表初始化(Direct-list-initialization)
T object { arg1, arg2, ... };
T object{.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (1)
T { arg1, arg2, ... };
T {.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (2)
new T { arg1, arg2, ... };
new T {.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (3)
Class { T member { arg1, arg2, ... }; };
Class { T member {.des1 = arg1 , .des2 { arg2 } ... }; }; // (C++20 起) (4)
Class::Class() : member { arg1, arg2, ... } { ... }
Class::Class() : member {.des1 = arg1 , .des2 { arg2 } ...} { ... } // (C++20 起) (5)
複製列表初始化(Copy-list-initialization)
T object = { arg1, arg2, ... };
T object = {.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (6)
function({ arg1, arg2, ... });
function({.des1 = arg1 , .des2 { arg2 } ... }); // (C++20 起) (7)
return { arg1, arg2, ... };
return {.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (8)
object[{ arg1, arg2, ... }];
object[{.des1 = arg1 , .des2 { arg2 } ... }]; // (C++20 起) (9)
object = { arg1, arg2, ... };
object = {.des1 = arg1 , .des2 { arg2 } ... }; // (C++20 起) (10)
U({ arg1, arg2, ... });
U({.des1 = arg1 , .des2 { arg2 } ... }); // (C++20 起) (11)
Class { T member = { arg1, arg2, ... }; };
Class { T member = {.des1 = arg1 , .des2 { arg2 } ... }; }; // (C++20 起) (12)
列表初始化的應用場景
List initialization is performed in the following situations:
direct-list-initialization (both explicit and non-explicit constructors are considered)
1) initialization of a named variable with a brace-enclosed initializer list
2) initialization of an unnamed temporary with a brace-enclosed initializer list
3) initialization of an object with dynamic storage duration with a new-expression, where the initializer is a brace-enclosed initializer list
4) in a non-static data member initializer that does not use the equals sign
5) in a member initializer list of a constructor if a brace-enclosed initializer list is used
copy-list-initialization (both explicit and non-explicit constructors are considered, but only non-explicit constructors may be called)
6) initialization of a named variable with a brace-enclosed initializer list after an equals sign
7) in a function call expression, with a brace-enclosed initializer list used as an argument and list-initialization initializes the function parameter
8) in a return statement with a brace-enclosed initializer list used as the return expression and list-initialization initializes the returned object
9) in a subscript expression with a user-defined operator[], where list-initialization initializes the parameter of the overloaded operator
10) in an assignment expression, where list-initialization initializes the parameter of the overloaded operator
11) functional cast expression or other constructor invocations, where a brace-enclosed initializer list is used in place of a constructor argument. Copy-list-initialization initializes the constructor's parameter (note; the type U in this example is not the type that is being list-initialized; U's constructor's parameter is)
12) in a non-static data member initializer that uses the equals sign
以下12種場景可與上面的12種語法一一對應。
直接列表初始化(direct-list-initialization,顯式(explicit)與非顯式(non-explicit)建構子都會被考慮):
- 使用大括號初始化一個具名變數(named variable)
- 使用大括號初始化一個不具名的臨時物件(unnamed temporary)
- new 表達式中,用大括號初始化動態儲存期的物件(an object with dynamic storage duration)
- 非靜態資料成員(non-static data member)的初始值設定(不使用等號的情況)
- 在建構子的成員初始化列表(member initializer list)中,用大括號初始化成員
複製列表初始化(copy-list-initialization,顯式與非顯式建構子都會被考慮,但只允許呼叫非顯式建構子):
- 使用等號後的大括號初始化具名變數
- 呼叫函數時,用大括號作為引數,列表初始化會初始化函數的參數
- return 語句中,用大括號作為返回值,列表初始化會初始化那個被返回的物件
- 如果註標運算式(subscript expression)中的
[]運算子是使用者自定義的,列表初始化會初始化該overloaded的運算子之參數 - 在指派運算式(assignment expression)中,列表初始化會初始化該overloaded的運算子之參數
- 在functional cast expression或用大括號作為建構子引數呼叫其它建構子時,複製列表初始化會初始化建構子的參數
- 非靜態資料成員初始值設定(使用等號的情況)
注1:未特別聲明的建構子都是non-explicit constructors,前面有加explicit關鍵字的才是explicit constructors。兩者的差別在於explicit constructor不會自動對引數做隱式的型別轉換,詳見Explicit Constructor in C++
注2:參考Why are the terms “automatic” and “dynamic” preferred over the terms “stack” and “heap” in C++ memory management?,dynamic表示物件的生命週期不是自動由編譯器控制,而是由程序員來掌控
注3:關於static data member,詳見C++ Static Data Members
注4:參考註標運算子:[],註標運算子(subscript Operator)即[]
注5:參考Explicit type conversion,functional cast expression即如:static_cast, reinterpret_cast或const_cast之類的轉型方法
解釋
Explanation
The effects of list-initialization of an object of type (possibly cv-qualified) T are:
If the brace-enclosed initializer list contains a designated initializer list and T is not a reference type, T must be an aggregate class. The ordered identifiers in the designators of the designated initializer list must form a subsequence of the ordered identifiers in the direct non-static data members of T. Aggregate initialization is performed.
(since C++20)
If T is an aggregate class and the brace-enclosed initializer list, which does not contain a designated initializer list,(since C++20) has a single initializer clause of the same or derived type (possibly cv-qualified), the object is initialized from that initializer clause (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization).
Otherwise, if T is a character array and the brace-enclosed initializer list has a single initializer clause that is an appropriately-typed string literal, the array is initialized from the string literal as usual.
Otherwise, if T is an aggregate type, aggregate initialization is performed.
Otherwise, if the brace-enclosed initializer list is empty and T is a class type with a default constructor, value-initialization is performed.
Otherwise, if T is a specialization of std::initializer_list, the object is initialized as described below.
Otherwise, if T is a class type, the constructors of T are considered, in two phases:
All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list.
If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the initializer clauses of the brace-enclosed initializer list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all).
Otherwise, if T is an enumeration type that with fixed underlying type U, the brace-enclosed initializer list has only one initializer v, and all following conditions are satisfied, then the enumeration is initialized with the result of converting v to U:
The initialization is direct-list-initialization.
v is of scalar type.
v is implicitly convertible to U.
The conversion from v to U is non-narrowing.
(since C++17)
Otherwise (if T is not a class type), if the brace-enclosed initializer list has only one initializer clause and either T is not a reference type or is a reference type whose referenced type is same as or is a base class of the type of the initializer clause, T is direct-initialized (in direct-list-initialization) or copy-initialized (in copy-list-initialization), except that narrowing conversions are not allowed.
Otherwise, if T is a reference type that is not compatible with the type of the initializer clause:
a prvalue temporary of the type referenced by T is copy-list-initialized, and the reference is bound to that temporary (this fails if the reference is a non-const lvalue reference).
(until C++17)
a prvalue is generated. The prvalue initializes its result object by copy-list-initialization. The prvalue is then used to direct-initialize the reference (this fails if the reference is a non-const lvalue reference). The type of the temporary is the type referenced by T, unless T is “reference to array of unknown bound of U”, in which case the type of the temporary is the type of x in the declaration U x[] H, where H is the initializer list(since C++20).
(since C++17)
Otherwise, if the brace-enclosed initializer list has no initializer clause, T is value-initialized.
對於型別(可能帶有 cv 限定(cv-qualified))T 的物件進行列表初始化時的效果如下:
-
如果大括號中的初始化列表(brace-enclosed initializer list)包含「指定初始化器列表」(designated initializer list),且 T 不是引用類型,那麼 T 必須是一個聚合類。指定初始化器列表中的指定初始化器(designators)中的identifier(識別字、標識符)必須形成 T 的直接非靜態資料成員(direct non-static data members)的identifier序列中的一個子序列。執行聚合初始化。
(自 C++20 起) -
如果 T 是聚合類,且大括號中的初始化列表不包含指定初始化器列表(自 C++20 起),並且該列表只有一個與 T 相同或派生型別(可能帶有 cv 限定)的初始化子句,則物件會由該初始化子句來初始化:
- 如果是 copy-list-initialization 則使用複製初始化(copy-initialization)
- 如果是 direct-list-initialization 則使用直接初始化(direct-initialization)
-
否則,如果 T 是字元陣列,且大括號中只有一個適當型別的字串常量(appropriately-typed string literal)的初始化子句,則陣列由該字串常量初始化(與傳統方式相同)。
-
否則,如果 T 是聚合型別則執行聚合初始化。
-
否則,如果大括號是空的,且 T 是帶有預設建構子(default constructor)的類型,則會進行值初始化(value-initialization)。
-
否則,如果 T 是
std::initializer_list的特化(a specialization of std::initializer_list),則物件會按照C++中的initializer_list處的說明進行初始化。 -
否則,如果 T 是一個類別(class type),則建構子會分兩階段被考慮:
- overload resolution會先檢查所有
std::initializer_list是唯一參數的建構子,或者第一個參數是std::initializer_list且其餘參數有預設值的建構子,接著將這些建構子與該std::initializer_list型別的引數做匹配。 - 如果第一階段沒有匹配成功,則 T 的所有建構子都會參與overload resolution,與包含 大括號中的初始化子句 在內的所有引數進行匹配。限制是:僅允許非縮窄轉換(non-narrowing conversions)。如果這個階段為copy-list-initialization選出的最佳匹配是一個 explicit 建構子,則編譯失敗。注意:在簡單 copy-initialization(simple copy-initialization) 中,explicit 建構子根本不會被考慮。
- overload resolution會先檢查所有
-
否則,如果 T 是一個枚舉型別(enumeration type),且有固定的底層型別 U,並且大括號中只有一個初始化子(initializer)
v,且滿足以下所有條件,則該枚舉型別會用v轉換為 U 的結果初始化 (自 C++17 起):- 初始化方式必須是 direct-list-initialization
v是純量型別(scalar type)v可被隱式轉換為 U- 該轉換不是縮窄轉換
-
否則(如果 T 不是class type),若大括號只有一個初始化子句,並且滿足以下條件之一:
- T 不是引用類型
- 或 T 是引用類型,且其參考的型別與初始化子句相同,或者是初始化子句型別的父類別
那麼:
- 在 direct-list-initialization 情況下會對T進行直接初始化(direct-initialized)
- 在 copy-list-initialization 情況下會對T進行複製初始化(copy-initialized)
- 但禁止縮窄轉換(narrowing conversions)
-
否則,如果 T 是引用類型,且與初始化子句的型別不相容:
-
(C++17 以前) 會copy-list-initialize一個 型別為 T 所參照的型別 之 prvalue 臨時物件,然後將該引用綁定到該臨時物件(若引用是非常量左值引用(non-const lvalue reference)則會失敗)。
-
(C++17 起) 會生成一個 prvalue,該 prvalue 使用 copy-list-initialization 初始化其結果物件。然後用prvalue來直接初始化(direct-initialize)該引用(若引用是非常量左值引用(non-const lvalue reference)則會失敗)。
臨時物件的型別通常是 T 所參照的型別,除非 T 是「指向未知長度陣列 U(array of unknown bound of
U) 的參照」,此時臨時物件型別是為U x[] H宣告中的 x 的型別,其中 H 為初始化列表。(C++20 起)
-
-
否則,如果大括號中沒有初始化子句,則 T 會進行值初始化。
注1:參考cv (const and volatile) type qualifiers,cv-qualified 即帶有const或volatile修飾字
注2:參考What is a designated initializer in C?,指定初始化器(designated initializer)是C++20新推出的初始化方式。
注3:關於聚合類(aggregate class),詳見Aggregate initialization官方文檔
注4:參考Copy-initialization,copy-initialization是從另一個物件初始化一個物件,包含以下這幾種方式:
T object = other; (1)
T object = {other}; (2) (until C++11)
f(other) (3)
return other; (4)
throw object;
catch (T object) (5)
T array[N] = {other-sequence}; (6)
注5:參考Direct-initialization,direct-initialization是從一組明確指定的建構子參數初始化一個物件,包含以下這幾種方式:
T object ( arg );
T object ( arg1, arg2, ... ); (1)
T object { arg }; (2) (since C++11)
T ( other )
T ( arg1, arg2, ... ) (3)
static_cast< T >( other ) (4)
new T( args, ... ) (5)
Class::Class() : member( args, ... ) { ... } (6)
[arg]() { ... } (7) (since C++11)
注6:參考Value-initialization,在用空的initializer建構物件時會執行值初始化(value-initialization),包含以下這幾種方式:
T () (1)
new T () (2)
Class::Class(...) : member () { ... } (3)
T object {}; (4) (since C++11)
T {} (5) (since C++11)
new T {} (6) (since C++11)
Class::Class(...) : member {} { ... } (7) (since C++11)
注7:參考Overload resolution,當有多個函數同名時,編譯器必須事先做name lookup把最有可能的版本找出來,這個過程稱作overload resolution。
注8:關於narrowing conversions詳見Narrowing conversions in C++ 11
範例程式
為了探討vector是如何由大括號初始化的,編寫一個C++檔案demo.cpp如下:
#include <iostream>
#include <vector>
class Animal {
public:
Animal(int f, bool t) : feet(f), tail(t) {
std::cout << "default construct " << f << ", " << t << std::endl;
}
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()
{
std::cout << "a1" << std::endl;
Animal a1 = { 4, true };
std::cout << "==========================================" << std::endl;
std::cout << "a2" << std::endl;
Animal a2 = a1;
std::cout << "==========================================" << std::endl;
std::cout << "a3" << std::endl;
Animal a3 = std::move(a2);
std::cout << "==========================================" << std::endl;
std::cout << "list initialization, default constructor" << std::endl;
std::vector<Animal> vec_animal = { {4, true}, {2, false} };
std::cout << "==========================================" << std::endl;
std::cout << "list initialization, copy constructor" << std::endl;
std::vector<Animal> vec_animal2 = { a1, a2 };
std::cout << "==========================================" << std::endl;
std::cout << "list initialization, move constructor" << std::endl;
std::vector<Animal> vec_animal3 = { std::move(a1), std::move(a2) };
std::cout << "==========================================" << std::endl;
std::cout << "direct list initialization, default constructor" << std::endl;
std::vector<Animal> vec_animal_direct{ {4, true}, {2, false} };
std::cout << "==========================================" << std::endl;
std::cout << "direct list initialization, copy constructor" << std::endl;
std::vector<Animal> vec_animal_direct2 = { a1, a2 };
std::cout << "==========================================" << std::endl;
std::cout << "direct list initialization, move constructor" << std::endl;
std::vector<Animal> vec_animal_direct3 = { std::move(a1), std::move(a2) };
std::cout << "==========================================" << std::endl;
return 0;
}
程式首先展示了Animal物件的三種初始化方式:default, copy和move。這部份的執行結果如下:
a1
default construct 4, 1
==========================================
a2
copy construct 4, 1
==========================================
a3
move construct 4, 1
==========================================
程式的第二部分使用官方文檔中的第(6)種語法對vector做Copy-list-initialization:
list initialization, default constructor
default construct 4, 1
default construct 2, 0
copy construct 4, 1
copy construct 2, 0
==========================================
list initialization, copy constructor
copy construct 4, 1
copy construct 0, 0
copy construct 4, 1
copy construct 0, 0
==========================================
list initialization, move constructor
move construct 4, 1
move construct 0, 0
copy construct 4, 1
copy construct 0, 0
==========================================
程式的最後一部分使用官方文檔中的第(1)種語法對vector做Direct-list-initialization:
direct list initialization, default constructor
default construct 4, 1
default construct 2, 0
copy construct 4, 1
copy construct 2, 0
==========================================
direct list initialization, copy constructor
copy construct 0, 0
copy construct 0, 0
copy construct 0, 0
copy construct 0, 0
==========================================
direct list initialization, move constructor
move construct 0, 0
move construct 0, 0
copy construct 0, 0
copy construct 0, 0
==========================================
從執行結果中可以看到每建構一個Animal的vector,Animal的constructor就會被呼叫四次。
但程式中的brace-enclosed initializer list中明明只有兩個元素,為何constructor卻會被呼叫四次呢?這得一步一步說起。
具體流程
參考List-initialization文檔中的例子,list initialization的具體流程可整理如下:
- 生成一個暫時的常量陣列(backing array)
- 由常量陣列建構
initializer_list - 編譯器調用
std::vector的 接受initializer_list的constructor
參考std::initializer_list自動被構建的條件一,這個流程成立的條件就是vector必須要有接受initializer_list建構子,我們可以到vector的官方文檔確認看看。
vector constructor簽名
從C++ doc - std::vector<T,Allocator>::vector中,可以看到vector確實提供了接受initializer_list的建構子:
vector( std::initializer_list<T> init,
const Allocator& alloc = Allocator() );
(11) (since C++11)
因此我們可以確認範例中vector的初始化方式確實是list initialization。
細分步驟
接著一步一步來看demo.cpp中的vec_animal3是如何由大括號初始化的:
std::vector<Animal> vec_animal3 = { std::move(a1), std::move(a2) };
建構backing array
在建構initializer_list前,編譯器會先建構一個常量陣列,作為initializer_list的backing array。
在本例中這個步驟相當於:
const Animal __a[2] = { std::move(a1), std::move(a2) };
當中會呼叫Animal的move constructor建構兩個Animal物件,作為initializer_list的backing array裡的元素。這部分的輸出如下:
move construct 4, 1
move construct 0, 0
注意到在建構vec_animal3的過程中,constructor總共被呼叫了四次,其中前兩次的呼叫便是在建構backing array中的兩個元素。
建構initializer_list
上一步的大括號會被編譯器視為initializer_list的backing array,編譯器會對這個backing array做包裝,得到一個initializer_list。
這個包裝的過程中並不涉及元素的複製,只是將initializer_list的成員變數const E* _M_array設為backing array的地址;將成員變數size_t _M_len設為backing array的長度而已。詳見List-initialization文檔中的例子。
接著來查看vector建構子的原始碼,看vector是如何由initializer_list建構出來的。
vector constructor原始碼
GCC編譯器對vector建構子的實現位於libstdc++ - stl_vector.h:
00297 /**
00298 * @brief Builds a %vector from an initializer list.
00299 * @param l An initializer_list.
00300 * @param a An allocator.
00301 *
00302 * Create a %vector consisting of copies of the elements in the
00303 * initializer_list @a l.
00304 *
00305 * This will call the element type's copy constructor N times
00306 * (where N is @a l.size()) and do no memory reallocation.
00307 */
00308 vector(initializer_list<value_type> __l,
00309 const allocator_type& __a = allocator_type())
00310 : _Base(__a)
00311 {
00312 _M_range_initialize(__l.begin(), __l.end(),
00313 random_access_iterator_tag());
00314 }
參考C++中的initializer_list - begin/end,initializer_list有begin() 和 end() 兩個iterator。程式將這兩個iterator當作參數傳入_M_range_initialize。
_M_range_initialize有兩個版本,兩者的差別在於支援的迭代器不同,一個支援std::input_iterator_tag,另一個支援std::forward_iterator_tag。
注:參考std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, std::random_access_iterator_tag, std::contiguous_iterator_tag,C++ named requirements: LegacyInputIterator和C++ named requirements: LegacyForwardIterator
std::input_iterator_tag即LegacyInputIterator,是能力最弱的一種可讀取迭代器,只能單向讀取一次。std::forward_iterator_tag即LegacyForwardIterator,繼承自std::input_iterator_tag,比std::input_iterator_tag稍強,支援多次讀取。
第一個版本接受LegacyInputIterator(因為LegacyForwardIterator繼承自LegacyInputIterator,所以也接受LegacyForwardIterator),實作如下:
01094 // Called by the second initialize_dispatch above
01095 template<typename _InputIterator>
01096 void
01097 _M_range_initialize(_InputIterator __first,
01098 _InputIterator __last, std::input_iterator_tag)
01099 {
01100 for (; __first != __last; ++__first)
01101 push_back(*__first);
01102 }
入參__first和__last為容器的iterator之起點和終點。
函數內部利用iterator來遍歷容器,並逐一將元素 push_back(也就是複製) 到 vector 的內部儲存空間中。
第二個版本接受LegacyForwardIterator(不接受LegacyInputIterator),實作如下:
01104 // Called by the second initialize_dispatch above
01105 template<typename _ForwardIterator>
01106 void
01107 _M_range_initialize(_ForwardIterator __first,
01108 _ForwardIterator __last, std::forward_iterator_tag)
01109 {
01110 const size_type __n = std::distance(__first, __last);
01111 this->_M_impl._M_start = this->_M_allocate(__n);
01112 this->_M_impl._M_end_of_storage = this->_M_impl._M_start + __n;
01113 this->_M_impl._M_finish =
01114 std::__uninitialized_copy_a(__first, __last,
01115 this->_M_impl._M_start,
01116 _M_get_Tp_allocator());
01117 }
首先使用this->_M_allocate函數尋找一塊空間,並把該空間分配給this->_M_impl._M_start,也就是vector容器本身的起點。
std::__uninitialized_copy_a用於將範圍[__first,__last)複製到新分配的空間this->_M_impl._M_start中,如果查看其源碼(位於stl_uninitialized.h),會發現它內部調用了std::uninitialized_copy。
std::uninitialized_copy接受的迭代器種類為LegacyForwardIterator,不接受LegacyInputIterator。也因此第二個版本的_M_range_initialize函數才需要限制模板參數的型別為_ForwardIterator。
可以看到不論是哪個版本的_M_range_initialize,內部都是透過複製來將backing array中的元素搬到vector中的。
因此執行結果中後兩行的輸出才是copy construct:
copy construct 4, 1
copy construct 0, 0
為何是用copy而不是move
在initializer_list很長的情況下,如果使用move應該能節省不少時間,那麼為何在vector的建構子中要用copy呢?
參考initializer_list的使用案例 - 用來初始化容器,vector 從 initializer_list 拿元素時,因為拿到的元素是 const T*,只能 copy,不能 move,所以只能呼叫 copy constructor 將元素放入 vector 內部儲存空間。
這就是為什麼即使大括號中的元素是經過std::move後的 rvalue:
std::vector<Animal> vec_animal3 = { std::move(a1), std::move(a2) };
但vector 內部卻仍然呼叫 copy constructor的原因。
如果想真正 move 進 vector,得避開 initializer_list 這個「陷阱」,改寫成:
std::vector<Animal> vec;
vec.push_back(std::move(a1));
vec.push_back(std::move(a2));
這樣才會直接調用 move constructor。
更多推荐

所有评论(0)