章节目录

std::array与类类型元素

本节阅读量:

数组不限于只能存放基本类型的元素。相反,std::array的元素可以是任何对象类型,包括复合类型。这意味着您可以创建指针的std::array,或结构体(或class)的std:::array

然而,初始化结构或类的std::array往往会绊倒新的程序员,因此我们将花一节课来明确地讨论这个主题。


定义并分配结构体给std::array

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

1
2
3
4
5
6
struct House
{
    int number{};
    int stories{};
    int roomsPerStory{};
};

定义House的std::array并分配元素的工作方式与您预期的一样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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;
}

上述输出如下:

1
2
3
House number 13 has 7 rooms.
House number 14 has 10 rooms.
House number 15 has 8 rooms.

初始化元素为结构体的std::array

只要明确了元素类型,初始化也可以像预期的那样工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <array>
#include <iostream>

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

int main()
{
    constexpr std::array houses { // 使用CTAD去推导模版参数类型为 <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;
}

在上面的示例中,使用CTAD将std::array的类型推断为std::array<House,3>。然后,提供3个House对象作为初始值设定项,这非常好。


初始化,而不显式指定每个初始值设定项的元素类型

在上面的示例中,您会注意到每个初始值设定项都要求我们列出元素类型:

1
2
3
4
5
    constexpr std::array houses {
            House{ 13, 1, 7 }, // 指定类型为 House
            House{ 14, 2, 5 }, // 这里也是
            House{ 15, 2, 4 }  // 这里也是
        };

但在分配案例中,不必这样做:

1
2
3
4
5
    // 编译器知道house中的每个元素类型为 House
    // 因此隐式的将右边的每个元素转换为 House
    houses[0] = { 13, 1, 7 };
    houses[1] = { 14, 2, 5 };
    houses[2] = { 15, 2, 4 };

所以你可能会想试试这样的方法:

1
2
3
4
5
6
    // 无法按预期工作
    constexpr std::array<House, 3> houses { // 告诉编译器每个元素都是一个 House
            { 13, 1, 7 }, // 这里不再声明元素类型
            { 14, 2, 5 },
            { 15, 2, 4 } 
        };

也许令人惊讶的是,这不起作用。让我们探索一下原因。

简要来说,std::array是定义为包含一个C样式数组的结构体,如下所示:

1
2
3
4
5
template<typename T, std::size_t N>
struct array
{
    T implementation_defined_name[N]; // C样式数组,有N个T类型的元素
}

因此,当我们尝试根据上面的内容初始化House时,编译器将如下解释初始化:

1
2
3
4
5
6
// 无法按预期工作
constexpr std::array<House, 3> houses { // 初始化 houses
    { 13, 1, 7 }, // 初始化 C 样式数组 implementation_defined_name
    { 14, 2, 5 }, // ?
    { 15, 2, 4 }  // ?
};

编译器将把{13,1,7}解释为houses的第一个成员的初始值设定项,这是具有实现定义名称的C样式数组。这将使用{13,1,7}初始化C样式数组元素0,其余成员将被零初始化。然后编译器会发现我们又提供了两个初始化值({14,2,5}和{15,2,4}),并产生一个编译错误,告诉我们提供了太多的初始化值。

初始化上述的正确方法是添加一组额外的大括号,如下所示:

1
2
3
4
5
6
7
8
// 按预期工作
constexpr std::array<House, 3> houses { // 初始化 houses
    { // 额外的大括号,用来初始化作为成员变量的C样式数组 implementation_defined_name
        { 13, 4, 30 }, // 初始化元素 0
        { 14, 3, 10 }, // 初始化元素 1
        { 15, 3, 40 }, // 初始化元素 2
     }
};

请注意所需的额外大括号(在std::array结构体中开始的C样式数组成员的初始化)。在这个大括号中,可以单独初始化每个元素,每个元素都在自己的大括号中。

这就是为什么当元素类型需要值列表,并且我们没有显式地将元素类型作为初始值设定项的一部分提供时,将看到带有一组额外大括号的std::array初始化方式。

下面是一个完整的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <array>
#include <iostream>

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

int main()
{
    constexpr std::array<House, 3> houses {{ // 注意这里有两个大括号
        { 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;
}

大括号的省略

鉴于上述解释,您可能会想知道为什么上面的情况需要双大括号,但我们看到的所有其他情况都只需要单大括号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <array>
#include <iostream>

int main()
{
    constexpr std::array<int, 5> arr { 1, 2, 3, 4, 5 }; // 单个大括号

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

    return 0;
}

事实证明,您可以为此类数组提供双大括号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <array>
#include <iostream>

int main()
{
    constexpr std::array<int, 5> arr {{ 1, 2, 3, 4, 5 }}; // 双大括号

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

    return 0;
}

C++中的聚合支持一个名为大括号省略的概念,它为何时可以省略多个大括号制定了一些规则。通常,在使用单个值初始化std::array时,或者在使用类类型或数组进行初始化时,可以省略大括号,其中每个元素必须显式带上类型。

始终用双大括号初始化std::array没有坏处,因为它避免了必须考虑大括号省略是否适用于特定情况。或者,您可以尝试使用单大括号初始化,如果编译器无法识别它,它通常会告警。在这种情况下,可以快速添加一组额外的大括号。


另一个例子

这里还有一个例子,我们用Student结构初始化std::array。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <array>
#include <iostream>
#include <string_view>

// 每个学生有一个 id 和 name
struct Student
{
	int id{};
	std::string_view name{};
};

// 有 3 个学生的数组 (因为这里指明了类型,所以单括号足够了)
constexpr std::array students{ Student{0, "Alex"}, Student{ 1, "Joe" }, Student{ 2, "Bob" } };

const Student* findStudentById(int id)
{
	// 遍历所有学生
	for (auto& s : students)
	{
		// 返回指定id的学生
		if (s.id == id) return &s;
	}

	// 没有匹配到对应的id
	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;
}

这将打印:

1
2
You found: Joe
You found: nobody

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


17.2 std::array作为函数参数或返回值

上一节

17.4 通过std::reference_wrapper创建引用的数组

下一节