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)建構子都會被考慮):

  1. 使用大括號初始化一個具名變數(named variable)
  2. 使用大括號初始化一個不具名的臨時物件(unnamed temporary)
  3. new 表達式中,用大括號初始化動態儲存期的物件(an object with dynamic storage duration)
  4. 非靜態資料成員(non-static data member)的初始值設定(不使用等號的情況)
  5. 在建構子的成員初始化列表(member initializer list)中,用大括號初始化成員

複製列表初始化(copy-list-initialization,顯式與非顯式建構子都會被考慮,但只允許呼叫非顯式建構子):

  1. 使用等號後的大括號初始化具名變數
  2. 呼叫函數時,用大括號作為引數,列表初始化會初始化函數的參數
  3. return 語句中,用大括號作為返回值,列表初始化會初始化那個被返回的物件
  4. 如果註標運算式(subscript expression)中的[]運算子是使用者自定義的,列表初始化會初始化該overloaded的運算子之參數
  5. 在指派運算式(assignment expression)中,列表初始化會初始化該overloaded的運算子之參數
  6. 在functional cast expression或用大括號作為建構子引數呼叫其它建構子時,複製列表初始化會初始化建構子的參數
  7. 非靜態資料成員初始值設定(使用等號的情況)

注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),則建構子會分兩階段被考慮:

    1. overload resolution會先檢查所有std::initializer_list是唯一參數的建構子,或者第一個參數是std::initializer_list且其餘參數有預設值的建構子,接著將這些建構子與該 std::initializer_list 型別的引數做匹配。
    2. 如果第一階段沒有匹配成功,則 T 的所有建構子都會參與overload resolution,與包含 大括號中的初始化子句 在內的所有引數進行匹配。限制是:僅允許非縮窄轉換(non-narrowing conversions)。如果這個階段為copy-list-initialization選出的最佳匹配是一個 explicit 建構子,則編譯失敗。注意:在簡單 copy-initialization(simple copy-initialization) 中,explicit 建構子根本不會被考慮。
  • 否則,如果 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。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐