17-2 std::array 的长度与索引
在第16.3节——std::vector与无符号长度及下标问题中,我们讨论了标准库容器类采用无符号值表示长度和索引这一不恰当的设计决策。由于std::array同样属于标准库容器类,它也面临相同的问题。
本节将回顾 std::array 的索引与长度获取方式。由于 std::vector 和 std::array 接口相似,相关内容将与 std::vector 的讲解保持一致。但鉴于仅 std::array 完全支持 constexpr,我们将对此进行更深入的探讨。
继续之前,建议先回顾“符号转换属于缩窄转换,除非在 constexpr 条件下”(参见第 16.3 节——std::vector 的无符号长度与下标问题)。
std::array 的长度类型为 std::size_t
std::array 实现为模板结构体,其声明如下:
template<typename T, std::size_t N> // N is a non-type template parameter
struct array;
如您所见,表示数组长度(N)的非类型模板参数具有类型 std::size_t。您可能已经知道,std::size_t 是一种大型无符号整数类型。
相关内容:
我们在第13.13节讲解了类模板(包含结构体模板),并在第11.9节探讨了非类型模板参数。
因此,定义std::array时,长度参数必须是std::size_t类型,或可转换为该类型值。由于该值必须满足 constexpr 要求,使用带符号整型值时不会引发符号转换问题——编译器会在编译时将带符号整型值转换为 std::size_t,且不会视为窄化转换。
顺带一提...:
在C++23之前,C++甚至没有为std::size_t提供字面量后缀,因为当需要constexpr std::size_t时,从int到std::size_t的隐式编译时转换通常已足够。添加后缀的主要目的是为了类型推导,因为 constexpr auto x { 0 } 会生成 int 而不是 std::size_t。在这种情况下,能够区分 0(int)和 0UZ(std::size_t)而不必显式使用 static_cast 非常有用。
std::array 的长度和索引类型为 size_type,该类型始终为 std::size_t
与 std::vector 类似,std::array 定义了一个名为 size_type 的嵌套 typedef 成员,该类型是容器长度(及索引类型,若支持)的别名。在 std::array 中,size_type 始终是 std::size_t 的别名。
需注意:定义 std::array 长度的非类型模板参数被显式定义为 std::size_t 而非 size_type。这是因为 size_type 是 std::array 的成员类型,在该处尚未被定义。此处是唯一显式使用 std::size_t 的位置——其余所有场景均使用 size_type。
获取 std::array 的长度
有三种常见方法可获取 std::array 对象的长度。
首先,我们可以使用 size() 成员函数查询 std::array 对象的长度(该函数返回长度值,类型为 unsigned size_type):
#include <array>
#include <iostream>
int main()
{
constexpr std::array arr { 9.0, 7.2, 5.4, 3.6, 1.8 };
std::cout << "length: " << arr.size() << '\n'; // returns length as type `size_type` (alias for `std::size_t`)
return 0;
}
这将输出:

与同时提供 length() 和 size() 成员函数(功能相同)的 std::string 和 std::string_view 不同,std::array(以及 C++ 中大多数其他容器类型)仅提供 size() 函数。
其次,在C++17中,我们可以使用非成员函数std::size()(对于std::array而言,该函数仅调用size()成员函数,从而将长度以unsigned size_type类型返回)。
#include <array>
#include <iostream>
int main()
{
constexpr std::array arr{ 9, 7, 5, 3, 1 };
std::cout << "length: " << std::size(arr); // C++17, returns length as type `size_type` (alias for `std::size_t`)
return 0;
}

最后,在C++20中,我们可以使用非成员函数std::ssize(),该函数返回长度值,类型为大型有符号整数类型(通常为std::ptrdiff_t):
#include <array>
#include <iostream>
int main()
{
constexpr std::array arr { 9, 7, 5, 3, 1 };
std::cout << "length: " << std::ssize(arr); // C++20, returns length as a large signed integral type
return 0;
}

这是三个函数中唯一一个将长度返回为有符号类型的函数。
获取 std::array 的长度作为 constexpr 值
由于 std::array 的长度是 constexpr 的,上述每个函数都会返回 std::array 的长度作为 constexpr 值(即使在非 constexpr 的 std::array 对象上调用时也是如此)!这意味着我们可在常量表达式中使用这些函数,且返回的长度可隐式转换为 int 类型而不构成窄化转换:
#include <array>
#include <iostream>
int main()
{
std::array arr { 9, 7, 5, 3, 1 }; // note: not constexpr for this example
constexpr int length{ std::size(arr) }; // ok: return value is constexpr std::size_t and can be converted to int, not a narrowing conversion
std::cout << "length: " << length << '\n';
return 0;
}

Visual Studio 用户注意:
Visual Studio 会错误地为上述示例触发警告 C4365。该问题已上报给微软。
警告:
由于语言缺陷,当通过(const)引用传递 std::array 函数参数时,调用上述函数将返回非 constexpr 值:
#include <array>
#include <iostream>
void printLength(const std::array<int, 5> &arr)
{
constexpr int length{ std::size(arr) }; // compile error!
std::cout << "length: " << length << '\n';
}
int main()
{
std::array arr { 9, 7, 5, 3, 1 };
constexpr int length{ std::size(arr) }; // works just fine
std::cout << "length: " << length << '\n';
printLength(arr);
return 0;
}
是个警告,但是可以通过编译并成功运行
该缺陷已在C++23的P2280提案中得到解决。截至本文撰写时,目前支持此功能的编译器寥寥无几。
一种解决方法是将foo()定义为函数模板,其中数组长度作为非类型模板参数。该非类型模板参数随后可在函数内部使用。我们将在第17.3节——传递和返回std::array中进一步探讨此方案。
template <auto Length>
void printLength(const std::array<int, Length> &arr)
{
std::cout << "length: " << Length << '\n';
}
使用运算符[]或at()成员函数对std::array进行下标访问
在之前的第17.1节——std::array简介中,我们提到访问std::array最常见的方式是使用下标运算符(operator[])。这种情况下不会进行边界检查,传递无效索引将导致未定义行为。
与std::vector类似,std::array也提供at()成员函数,该函数在运行时进行边界检查。我们建议避免使用此函数,因为通常需要在索引前进行边界检查,或需要编译时边界检查。
这两种函数均要求索引类型为size_type(即std::size_t)。
若使用 constexpr 值调用上述任一函数,编译器将执行 constexpr 转换至 std::size_t。此转换不被视为缩窄转换,因此不会引发符号问题。
但若使用非 constexpr 的带符号整数调用这些函数,转换为 std::size_t 则会被视为缩窄转换,编译器可能发出警告。我们在第 16.3 课——std::vector 与无符号长度及下标问题中,将通过 std::vector 的实例进一步探讨此情况。
std::get() 对常量表达式索引进行编译时边界检查
由于 std::array 的长度是常量表达式,若索引值同样为常量表达式,编译器应能在编译时验证该常量表达式索引是否在数组边界内(若索引超出边界则终止编译)。
然而,根据定义,operator[] 不会进行边界检查,而 at() 成员函数仅在运行时进行边界检查。此外,函数参数不能是 constexpr(即使对于 constexpr 或 consteval 函数也是如此),那么我们如何传递 constexpr 索引呢?
要实现常量表达式索引的编译时边界检查,可使用std::get()函数模板,该模板将索
#include <array>
#include <iostream>
int main()
{
constexpr std::array prime{ 2, 3, 5, 7, 11 };
std::cout << std::get<3>(prime); // print the value of element with index 3
std::cout << std::get<9>(prime); // invalid index (compile error)
return 0;
}

在 std::get() 的实现内部,存在一个 static_assert 用于检查非类型模板参数是否小于数组长度。若不满足此条件,static_assert 将因编译错误而终止编译过程。
由于模板参数必须为 constexpr,因此 std::get() 只能通过 constexpr 索引进行调用。
测验时间
问题 #1
初始化一个包含以下值的 std::array:‘h’、‘e’、‘l’、‘l’、‘o’。打印数组的长度,然后使用 operator[]、at() 和 std::get() 打印索引为 1 的元素值。
程序应输出:
The length is 5
eee
显示解决方案
#include <array>
#include <iostream>
int main()
{
constexpr std::array arr { 'h', 'e', 'l', 'l', 'o' };
std::cout << "The length is " << std::size(arr) << '\n';
std::cout << arr[1] << arr.at(1) << std::get<1>(arr) << '\n';
return 0;
}




浙公网安备 33010602011771号