17-4 std::array 类型及花括号省略

std::array 并不局限于基本类型的元素。相反,std::array 的元素可以是任何对象类型,包括复合类型。这意味着你可以创建一个指针的 std::array,或一个结构体(或类)的 std::array。(或类)

然而,初始化结构体或类的 std::array 往往会让新手程序员感到困惑,因此我们将专门用一节课来详细讲解这个主题。

作者注:
本节课我们将使用结构体来阐述要点。这些内容同样适用于类。


定义并赋值一个包含结构体的std::array

我们从一个简单的结构体开始:

struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};

定义一个House类型的std::array数组并赋值的过程完全符合预期:

#include <array>
#include <iostream>

struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main()
{
    std::array<House, 3> houses{};

    houses[0] = { 13, 1, 7 };
    houses[1] = { 14, 2, 5 };
    houses[2] = { 15, 2, 4 };

    for (const auto& house : houses)
    {
        std::cout << "House number " << house.number
                  << " has " << (house.stories * house.roomsPerStory)
                  << " rooms.\n";
    }

    return 0;
}

上述操作输出如下:

image


初始化结构体的 std::array

初始化结构体数组的方式也完全符合预期,只要明确指定元素类型即可:

#include <array>
#include <iostream>

struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main()
{
    constexpr std::array houses { // use CTAD to deduce template arguments <House, 3>
            House{ 13, 1, 7 },
            House{ 14, 2, 5 },
            House{ 15, 2, 4 }
        };

    for (const auto& house : houses)
    {
        std::cout << "House number " << house.number
            << " has " << (house.stories * house.roomsPerStory)
            << " rooms.\n";
    }

    return 0;
}

image

在上例中,我们使用CTAD推断出std::array的类型为std::array<House, 3>。随后提供3个House对象作为初始化值,这完全可行。


初始化时未显式指定每个初始化器的元素类型

在上例中,您会注意到每个初始化器都需要我们列出元素类型:

constexpr std::array houses {
        House{ 13, 1, 7 }, // we mention House here
        House{ 14, 2, 5 }, // and here
        House{ 15, 2, 4 }  // and here
    };

但我们不必在赋值的情况下做同样的事情:

// The compiler knows that each element of houses is a House
// so it will implicitly convert the right hand side of each assignment to a House
houses[0] = { 13, 1, 7 };
houses[1] = { 14, 2, 5 };
houses[2] = { 15, 2, 4 };

所以你可能会想到尝试这样做:

// doesn't work
constexpr std::array<House, 3> houses { // we're telling the compiler that each element is a House
        { 13, 1, 7 }, // but not mentioning it here
        { 14, 2, 5 },
        { 15, 2, 4 }
    };

令人惊讶的是,这并不可行。让我们来探究原因。

std::array被定义为包含单个C风格数组成员(其名称由实现定义)的结构体,如下所示:

template<typename T, std::size_t N>
struct array
{
    T implementation_defined_name[N]; // a C-style array with N elements of type T
}

作者注:
我们尚未讲解C风格数组,但本节课只需了解:T implementation_defined_name[N]; 表示类型为T、大小为N的固定尺寸数组(与std::array<T, N> implementation_defined_name;完全等价)。

C风格数组将在后续第17.7节——C风格数组入门 中详细讲解。

因此,当我们尝试按上述方式初始化房屋时,编译器会将初始化解释为:

// Doesn't work
constexpr std::array<House, 3> houses { // initializer for houses
    { 13, 1, 7 }, // initializer for C-style array member with implementation_defined_name
    { 14, 2, 5 }, // ?
    { 15, 2, 4 }  // ?
};

image

编译器将把 { 13, 1, 7 } 解释为 houses 数组第一个成员的初始化表达式,该数组采用 C 风格且名称由实现定义。这将用 { 13, 1, 7 } 初始化 C 风格数组的第 0 个元素,其余成员将被零初始化。随后编译器会发现我们提供了两个额外的初始化值({ 14, 2, 7 } 和 { 15, 2, 5 }),并报出编译错误,提示初始化值数量过多。

正确的初始化方式是添加额外的花括号,如下所示:

// This works as expected
constexpr std::array<House, 3> houses { // initializer for houses
    { // extra set of braces to initialize the C-style array member with implementation_defined_name
        { 13, 4, 30 }, // initializer for array element 0
        { 14, 3, 10 }, // initializer for array element 1
        { 15, 3, 40 }, // initializer for array element 2
     }
};

image

请注意需要额外添加的一对大括号(用于在 std::array 结构体内部初始化 C 风格数组成员)。在此大括号内,我们可以分别初始化每个元素,每个元素都位于其自身的一对大括号中。

这就是为什么当元素类型需要值列表时,且我们在初始化器中未显式提供元素类型,会看到std::array初始化器中出现额外的一对大括号。

关键要点:
当使用结构体、类或数组初始化 std::array 时,若未在每个初始化器中指定元素类型,则需添加额外花括号,以便编译器正确解析初始化内容。

这是聚合初始化的特性,其他使用列表构造函数的标准库容器类型在此类情况下无需双花括号。

完整示例如下:

#include <array>
#include <iostream>

struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main()
{
    constexpr std::array<House, 3> houses {{ // note double braces
        { 13, 1, 7 },
        { 14, 2, 5 },
        { 15, 2, 4 }
    }};

    for (const auto& house : houses)
    {
        std::cout << "House number " << house.number
                  << " has " << (house.stories * house.roomsPerStory)
                  << " rooms.\n";
    }

    return 0;
}

image


大括号省略规则(适用于集合表达式)

基于上述说明,您可能疑惑为何上述情况需要双大括号,而我们之前遇到的所有其他情况仅需单大括号:

#include <array>
#include <iostream>

int main()
{
    constexpr std::array<int, 5> arr { 1, 2, 3, 4, 5 }; // single braces

    for (const auto n : arr)
        std::cout << n << '\n';

    return 0;
}

image

原来你可以为这类数组提供双花括号:

#include <array>
#include <iostream>

int main()
{
    constexpr std::array<int, 5> arr {{ 1, 2, 3, 4, 5 }}; // double braces

    for (const auto n : arr)
        std::cout << n << '\n';

    return 0;
}

image

然而,C++中的聚合类型支持一种称为花括号省略的概念,该机制规定了在何种情况下可省略多个花括号。通常,当使用标量(单个)值初始化std::array时,或使用类类型或数组初始化且每个元素类型均明确标注时,均可省略花括号。

始终使用双花括号初始化 std::array 并无不妥,这样可避免思考特定场景下花括号省略是否适用。若尝试使用单花括号初始化,编译器通常会在无法确定时发出警告。此时只需快速添加额外花括号即可。


另一个示例

这里再提供一个示例,其中我们使用Student结构体初始化std::array。

#include <array>
#include <iostream>
#include <string_view>

// Each student has an id and a name
struct Student
{
	int id{};
	std::string_view name{};
};

// Our array of 3 students (single braced since we mention Student with each initializer)
constexpr std::array students{ Student{0, "Alex"}, Student{ 1, "Joe" }, Student{ 2, "Bob" } };

const Student* findStudentById(int id)
{
	// Look through all the students
	for (auto& s : students)
	{
		// Return student with matching id
		if (s.id == id) return &s;
	}

	// No matching id found
	return nullptr;
}

int main()
{
	constexpr std::string_view nobody { "nobody" };

	const Student* s1 { findStudentById(1) };
	std::cout << "You found: " << (s1 ? s1->name : nobody) << '\n';

	const Student* s2 { findStudentById(3) };
	std::cout << "You found: " << (s2 ? s2->name : nobody) << '\n';

	return 0;
}

这将输出:

image

请注意,由于 std::array students 是 constexpr,我们的 findStudentById() 函数必须返回 const 指针,这意味着 main() 中的 Student 指针也必须是 const。


测验时间

问题 #1

定义名为 Item 的结构体,包含两个成员:std::string_view 类型的 name 和 int 类型的 gold。定义一个 std::array 并用 4 个 Item 对象初始化。使用 CTAD 推断元素类型和数组大小。

显示提示

提示:您需要为每个初始化器显式指定元素类型。

程序应输出以下内容:

A sword costs 5 gold.
A dagger costs 3 gold.
A club costs 2 gold.
A spear costs 7 gold.

image

显示解答

#include <array>
#include <iostream>
#include <string_view>

struct Item
{
    std::string_view name {};
    int gold {};
};

template <std::size_t N>
void printStore(const std::array<Item, N>& arr)
{
    for (const auto& item: arr)
    {
        std::cout << "A " << item.name << " costs " << item.gold << " gold.\n";
    }
}

int main()
{
    constexpr std::array store { // CTAD, single braces due to brace elision
        Item { "sword",    5 },
        Item { "dagger",   3 },
        Item { "club",     2 },
        Item { "spear",    7 } };
    printStore(store);

    return 0;
}

由于我们为每个初始化器显式指定了元素类型,因此在此处可以使用CTAD和单花括号(因花括号省略)。


问题 #2

更新你对测验 1 的解答,使每个初始化器不再显式指定元素类型。

显示解答

#include <array>
#include <iostream>
#include <string_view>

struct Item
{
    std::string_view name {};
    int gold {};
};

template <std::size_t N>
void printStore(const std::array<Item, N>& arr)
{
    for (const auto& item: arr)
    {
        std::cout << "A " << item.name << " costs " << item.gold << " gold.\n";
    }
}

int main()
{
    constexpr std::array<Item, 4> store {{ // No CTAD, must use double braces
        { "sword",    5 },
        { "dagger",   3 },
        { "club",     2 },
        { "spear",    7 }
    }};
    printStore(store);

    return 0;
}

由于我们并未为每个初始化器显式指定元素类型,因此无法使用CTAD(类型推断)或花括号省略。这意味着必须使用双花括号。

posted @ 2026-01-10 16:02  游翔  阅读(24)  评论(0)    收藏  举报