C++中的Aggregate initialization
C++中的Aggregate initialization
- Aggregate initialization
- 定義
- 語法
- 專有名詞定義
- Aggregate
- Element
- 範例
- Animal類別
- 初始化Animal物件
- list initialization of std::vector
- aggregate initialization of std::array
- default constructor
- copy constructor
- move constructor
- vector為何無法用aggregate initialization
Aggregate initialization
參考Aggregate initialization官方文檔,Aggregate initialization定義如下。
定義
Initializes an aggregate from an initializer list. It is a form of list-initialization(since C++11).
使用 初始化(器)列表 (initializer list) 來初始化一個 聚合型別 (aggregate)。
這是 列表初始化 (list-initialization) 的一種形式(自 C++11 起引入)。
關於list initialization,詳見C++中的List-initialization。
語法
T object = { arg1, arg2, ... }; (1)
T object { arg1, arg2, ... }; (2) (since C++11)
T object = { .des1 = arg1 , .des2 { arg2 } ... }; (3) (since C++20)
T object { .des1 = arg1 , .des2 { arg2 } ... }; (4) (since C++20)
1,2) Initializing an aggregate with an ordinary initializer list.
3,4) Initializing an aggregate with designated initializers (aggregate class only).
1、2)使用普通的初始化列表(initializer list)來初始化聚合物件(aggregate)。
3、4)C++20 起:使用「指定初始化器」(designated initializers)來初始化聚合物件(僅適用於聚合類別)。
這裡說的聚合物件包括array和符合特定條件的class,用大括號包住array或class的元素後,可用它來就地(in-place)對array或class初始化,aggregate initialization 會把大括號中的每個值 依序對應到array的每個元素或class的每個成員。
專有名詞定義
Aggregate
Aggregate
An aggregate is one of the following types:array types
class types that has
no user-declared constructors
(until C++11)
no user-provided, inherited, or explicit constructors
(since C++11)
(until C++20)
no user-declared or inherited constructors
(since C++20)
no private or protected direct non-static data members
no base classes
(until C++17)
no virtual base classes
no private or protected direct base classes
(since C++17)
no virtual member functions
no default member initializers
(since C++11)
(until C++14)
聚合 (aggregate) 的定義
一個 aggregate 必須是下列型別之一:
- 陣列型別 (array types)
- 類別型別 (class types),且需滿足以下限制:
- 建構子條件
- C++11 以前:沒有使用者宣告的建構子 (user-declared constructors)
- C++11 ~ C++20:沒有使用者提供、繼承或
explicit的建構子 - C++20 起:沒有使用者宣告或繼承的建構子
- 成員存取條件
- 沒有
private或protected的直接非靜態資料成員(direct non-static data members)
- 沒有
- 繼承條件
- C++17 以前:不能有基底類別
- C++17 起:沒有虛擬基底類別 (virtual base classes)
- C++17 起:不能有
private或protected的直接基底類別(direct base classes) - 沒有虛擬成員函式 (virtual member functions)
- 成員初始值條件
- C++11 ~ C++14:不能有 default member initializers
- 建構子條件
注1:
- direct non-static data members:在類別裡直接宣告的非靜態資料成員
- indirect non-static data members:如果是從 基底類別(base class)繼承來的成員,那就不是 direct,而是來自基底。
注2:直接基底類別 (direct base classes)
參考Derived Classes in C++:
A direct base class is the base class from which a
derived class explicitly inherits.
An indirect base class is inherited from two or
more levels up in the class hierarchy.
直接基類(direct base class)是衍生類別明確繼承的基類。
間接基類(indirect base class)則是位於衍生類別的繼承階層中更上層(兩層或以上)的基類。
Element
Element
The elements of an aggregate are:for an array, the array elements in increasing subscript order, or
for a class, the non-static data members that are not anonymous bit-fields, in declaration order.
(until C++17)
for a class, the direct base classes in declaration order, followed by the direct non-static data members that are neither anonymous bit-fields nor members of an anonymous union, in declaration order.
(since C++17)
Element(聚合元素)
聚合(aggregate)的元素(elements)定義如下:
- 對於陣列 (array):
- 元素就是陣列的每個成員,按照 索引從小到大 的順序。
- 對於類別 (class):
- 直到C++17:聚合的元素是 非靜態資料成員 (non-static data members),排除匿名 bit-field,按宣告順序排列。
- C++17 起:聚合的元素先是 直接基底類別 (direct base classes),按宣告順序。然後是 直接非靜態資料成員,排除匿名 bit-field 或匿名 union 的成員,按宣告順序排列。
注1:bit-field
參考C++ Bit Fields:
Classes and structures can contain members that occupy less storage than an integral type. These members are specified as bit fields
類別(class)和結構(structure)中可以包含「佔用空間比整型(integral type)更少」的成員。這些成員稱為bit fields。
注2:anonymous union
參考Union declaration:
Anonymous unions
An anonymous union is an unnamed union definition that does not simultaneously define any variables (including objects of the union type, references, or pointers to the union).union { member-specification } ;
匿名聯合(anonymous union) 是一個沒有名稱的 union 定義,而且在定義的同時不會宣告任何變數(包括該 union 類型的物件、參考或指標)。
範例
我們看完了aggregate initialization的官方文檔,但aggregate initialization相比list initialization帶來了哪些好處呢?編寫一個範例程式來看看:
#include <iostream>
#include <vector>
#include <array>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 << "aggregate initialization, default constructor" << std::endl;std::array<Animal, 2> arr_animal = { { {4, true}, {2, false} } };// std::array<Animal, 2> arr_animal = { {4, true}, {2, false} }; // invalidstd::cout << "==========================================" << std::endl;std::cout << "aggregate initialization, copy constructor" << std::endl;std::array<Animal, 2> arr_animal2 = { { a1, a2 } };// std::array<Animal, 2> arr_animal2 = { a1, a2 }; // validstd::cout << "==========================================" << std::endl;std::cout << "aggregate initialization, move constructor" << std::endl;std::array<Animal, 2> arr_animal3 = { { std::move(a1), std::move(a2) } };// std::array<Animal, 2> arr_animal3 = { std::move(a1), std::move(a2) }; // validreturn 0;
}
Animal類別
程式中首先定義了一個名為Animal的class:
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;
};
當中定義了default, copy, move三種建構子,每種建構子在被調用時皆會有相對應的輸出。在接下來的程式中我們便可以透過輸出來判斷究竟是何種建構子正在被調用。
初始化Animal物件
接下來便實際透過default, copy, move三種constructor來分別初始化a1, a2, a3三個物件:
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;
這部分的運行結果如下:
a1
default construct 4, 1
==========================================
a2
copy construct 4, 1
==========================================
a3
move construct 4, 1
==========================================
- 注意到因為
Animal類別有自定義的建構子,不符合聚合 (aggregate) 的條件,所以無法對它使用aggregate initialization。此處a1的初始化方式是List-initialization,會呼叫default constructor完成初始化 a2透過=運算子呼叫copy constructor完成初始化a3:=運算子右邊的是經過std::move後的右值,所以move constructor會被呼叫
list initialization of std::vector
接下來用以下三種方式初始化std::vector<Animal>:
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;
運行結果如下:
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
==========================================
注意到在程式中明明每個vector都只有兩個元素,但是在運行結果中,Animal的constructor卻被呼叫了四次,這是為什麼呢?這部分的解析已獨立成文,詳見C++中的List-initialization。
aggregate initialization of std::array
因為std::vector只能使用list initialization,而list initialization本身的特性則會導致同一個元素被建構出來後還要再被多複製一次。因此才會看到上面大括號中明明只有兩個元素,建構子卻被呼叫四次的現象。
接著來看看相比list initialization,aggregate initialization有何優越之處。
default constructor
std::cout << "aggregate initialization, default constructor" << std::endl;
std::array<Animal, 2> arr_animal = { { {4, true}, {2, false} } };
// std::array<Animal, 2> arr_animal = { {4, true}, {2, false} }; // invalid
std::cout << "==========================================" << std::endl;
這個範例中用到了std::array,因為std::array屬於陣列型別 (array types),滿足聚合 (aggregate)的條件,因此可以套用aggregation initialization。
在初始化arr_animal時用了三對括號,比vector list initialization多了一對,其實這跟std::array的底層定義有關。根據17.4 — std::array of class types, and brace elision:
A std::array is defined as a struct that contains a single C-style array member (whose name is implementation defined), like this:
template<typename T, std::size_t N>
struct array
{T implementation_defined_name[N]; // a C-style array with N elements of type T
}
可以看到std::array本質上是一個包含單個元素的struct,而該元素則是一個T型別的C語言的陣列。
注意到外層的struct滿足聚合 (aggregate)對類別型別(class types)的條件,所以外層struct是為聚合(aggregate),可套用aggregate initialization;內部的implementation_defined_name則是陣列型別 (array types),因此它也是聚合 (aggregate),也可套用aggregate initialization。
所以初始化arr_animal時用的三對括號的作用分別是:
- 最外層的括號是用aggregate initialization來初始化
std::array所代表的struct本身。因為該struct只有implementation_defined_name一個元素,所以最外層括號內也只有一個元素 - 中層括號的作用是用aggregate initialization來初始化
implementation_defined_name。中層括號內的兩個元素會被用來初始化implementation_defined_name陣列所包含的兩個元素 - 最內層的括號則是用list-initialization來初始化
Animal,括號內的兩個元素會被當作參數傳給Animal的default constructor
這部份的運行結果如下:
aggregate initialization, default constructor
default construct 4, 1
default construct 2, 0
==========================================
可以看到相比於vector的list initialization,constructor被呼叫的次數減半,也就是說,對於一個Animal物件,constructor只被呼叫了一次,帶來了性能上的提升。
copy constructor
std::cout << "aggregate initialization, copy constructor" << std::endl;
std::array<Animal, 2> arr_animal2 = { { a1, a2 } };
// std::array<Animal, 2> arr_animal2 = { a1, a2 }; // valid
std::cout << "==========================================" << std::endl;
這段程式中仍用了多層括號:
- 最外層的括號是用aggregate initialization來初始化
std::array所代表的struct本身。因為該struct只有implementation_defined_name一個元素,所以最外層括號內也只有一個元素 - 內層括號的作用是用aggregate initialization來初始化
implementation_defined_name。內層括號內的兩個元素會被用來初始化implementation_defined_name陣列所包含的兩個元素 - 用Copy-initialization分別由
a1和a2初始化得到兩個Animal物件,過程中會呼叫Animal的copy constructor
這部份的運行結果如下:
aggregate initialization, copy constructor
copy construct 0, 0
copy construct 0, 0
==========================================
注:編譯器允許programmer省略最外層的括號,因此以下寫法也是有效的:
std::array<Animal, 2> arr_animal2 = { a1, a2 }; // valid
move constructor
std::cout << "aggregate initialization, move constructor" << std::endl;
std::array<Animal, 2> arr_animal3 = { { std::move(a1), std::move(a2) } };
// std::array<Animal, 2> arr_animal3 = { std::move(a1), std::move(a2) }; // valid
這段程式中也使用了多層括號,其意涵如下:
- 最外層的括號是用aggregate initialization來初始化
std::array所代表的struct本身。因為該struct只有implementation_defined_name一個元素,所以最外層括號內也只有一個元素 - 內層括號的作用是用aggregate initialization來初始化
implementation_defined_name。內層括號內的兩個元素會被用來初始化implementation_defined_name陣列所包含的兩個元素 std::move(a),std::move(b)這兩個右值分別被當作Animal的move constructor的參數,用於建構兩個Animal物件
這部份的運行結果如下:
aggregate initialization, move constructor
move construct 0, 0
move construct 0, 0
注:編譯器允許programmer省略最外層的括號,因此以下寫法也是有效的:
std::array<Animal, 2> arr_animal3 = { std::move(a1), std::move(a2) }; // valid
注意我們不能將=右側的{ std::move(a1), std::move(a2) }寫成std::move({ a, b })。
std::array本來可以從大括號直接初始化的,但是如果我們把=右側的東西寫成std::move({ a, b }),則它的型別會變成 std::initializer_list<Animal>&&。
因為std::array沒有接受 std::initializer_list 為參數的建構子,所以無法由std::move({ a, b })初始化std::array。
vector為何無法用aggregate initialization
最後來探討一下std::vector為何只能用list initialization,無法用aggregate initialization。
因為aggregate initialization只適用於聚合類型,而vector不是聚合類型,所以無法直接由大括號「就地」建構std::vector<Animal>內部的元素。
在建構std::vector的過程中,首先會建構一個initializer_list,再呼叫std::vector的接受initializer_list的建構子。在呼叫該建構子的過程中,因為initializer_list的iterator是const的,所以必須一一將initializer_list中的元素複製給std::vector(無法move),最終導致每個元素都需要被多複製一次。
