在嘗試建立一個放 pair 的 std::array 時,發現語法似乎跟想像中不太一樣,一查才發現跟一般的 list-initialization 差別還不小。

背景知識

在 C++ 中,一個 aggregate 的定義 (C++20) 為:

  1. array type
  2. class types that has
    • no user-declared or inherited constructors
    • no private or protected non-static data members
    • no virtual base classes
    • no private or protected direct base classes
    • no virtual member functions

以下的 struct 是一個 aggregate:

struct S {
    int x;
 
    struct Foo {
        int i;
        int j;
        int a[3];
    } b;
};

Aggregate 的 aggregate-initialization 是 list-initialization 的一種特殊形式,以下是幾個初始化 S 的範例:

S s1 = {1, {2, 3, {4, 5, 6}}};
S s2 = {1, 2, 3, 4, 5, 6};  // same, but with brace elision
S s3{1, {2, 3, {4, 5, 6}}}; // same, using direct-list-initialization syntax
S s4{1, 2, 3, 4, 5, 6};     // error until CWG 1270:
                            // brace elision only allowed with equals sign

比較特別的是 s2 省略了中間的大括號依然可以成功初始化,這是 aggregate-initialization 的特性之一。

正題

std::array 是只包含一個固定大小的 C-style array 的容器,類型為 aggregate,因此初始化是使用 aggregate-initialization。與同樣是 STL 的 std::vector 不同,沒有可以接收 initializer-list 類型的 constructor。

根據前面的範例,我們可以初始化一個簡單的 array:

std::array<int, 5> nums = {1, 2, 3, 4, 5};

但當我們想在 array 裡面放 pair 時,如果用與 vector 相同的方法初始化,則會出現錯誤:

std::array<std::pair<int, int>, 3> ids = {{0, 1}, {1, 2}, {2, 3}};
// error: too many initializers for 'std::array<std::pair<int, int>, 3>'

這是因為 std::array 的類型是 aggregate,而在 C++ 標準 9.4.2 中提到:

When an aggregate is initialized by an initializer list as specified in list-initialization (9.4.5), the elements of the initializer list are taken as initializers for the elements of the aggregate.

也就是最外層的大括號是 std::array 的 initializer,裡面的元素會分別作為 std::array 裡 data member 的 initializer。

如同前面所說,std::array<T, N> 只有一個 C-style array T[N] 的 data member。在上面的例子中,會有 3 個 initializer 給 std::array,但實際上只有一個 data member,才會產生上面的錯誤。

若只在大括號內放剛好一個 initializer 會發生什麼事?{0, 1} 會被當成裝有 pair 的 array 的 initializer,而 0 跟 1 分別被當成 pair 的 initializer,因此會產生以下錯誤:

std::array<std::pair<int, int>, 3> ids = {{0, 1}};
// error: could not convert '0' from 'int' to 'std::pair<int, int>'

所以正確的做法是在 {0, 1}, {1, 2}, {2, 3} 的外面再加一層大括號,使其整個成為 C-style array 的 initializer:

std::array<std::pair<int, int>, 3> ids = {{{0, 1}, {1, 2}, {2, 3}}};

上面 std::array<int, 5> 的例子之所以能成立,是因為觸發了大括號省略 (brace elision) 的規則,將大括號內以逗號分隔的 initializer-clause 用於初始化 subaggregate 的子元素,在這裡 subaggregate 就是指 std::array 內的 C-style array。

參考連結