page contents

C 语言中std::array的神奇用法总结

熟悉C 的人知道:C 的编译期处理大多可以用模板的trick来完成——因为模板参数一定是编译期常量。因此我们可以用模板参数来完成编译期处理——只要把数组元素全部作为模板的非类型参数就可以了

std::array是在C 11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与性能。也正因此,使得std::array有很多与其他容器不同的特殊之处,比如:std::array的元素是直接存放在实例内部,而不是在堆上分配空间;std::array的大小必须在编译期确定;std::array的构造函数、析构函数和赋值操作符都是编译器隐式声明的……这让很s多用惯了std::vector这类容器的程序员不习惯,觉得std::array不好用。


但实际上,std::array的威力很可能被低估了。在这篇文章里,我会从各个角度介绍下std::array的用法,希望能带来一些启发。


本文的代码都在C 17环境下编译运行。当前主流的g 版本已经能支持C 17标准,但是很多版本(如gcc 7.3)的C 17特性不是默认打开的,需要手工添加编译选项-std=c 17。


自动推导数组大小


很多项目中都会有类似这样的全局数组作为配置参数:



uint32_t g_cfgPara[] = {1, 2, 5, 6, 7, 9, 3, 4};


当程序员想要使用std::array替换原生数组时,麻烦来了:

array, 8> g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 注意模板参数“8”


程序员不得不手工写出数组的大小,因为它是std::array的模板参数之一。如果这个数组很长,或者经常增删成员,对数组大小的维护工作恐怕不是那么愉快的。有人要抱怨了:std::array的声明用起来还没有原生数组方便,选它干啥?

但是,这个抱怨只该限于C 17之前, C 17带来了类模板参数推导特性, 你不再需要手工指定类模板的参数:


array g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 数组大小与成员类型自动推导


看起来很美好,但很快就会有人发现不对头:数组元素的类型是什么?还是std::uint32_t吗?


有人开始尝试只提供元素类型参数,让编译器自动推导长度,遗憾的是,它不会奏效。

array g_cfgPara = {1, 2, 5, 6, 7, 9, 3, 4};  // 编译错误


好吧,暂时看起来std::array是不能像原生数组那样声明。下面我们来解决这个问题。


用函数返回std::array


问题的解决思路是用函数模板来替代类模板——因为C 允许函数模板的部分参数自动推导——我们可以联想到std::make_pair、std::make_tuple这类辅助函数。巧的是, C 标准真的在TS v2试验版本中推出过std::make_array, 然而因为类模板参数推导的问世,这个工具函数后来被删掉了。


但显然,用户的需求还是存在的。于是在C 20中, 又新增了一个辅助函数std::to_array。

别被C 20给吓到了,这个函数的代码其实很简单,我们可以把它拿过来定义在自己的C 17代码中[1]。



template<typename R, typename P, size_t N, size_t... I>constexpr array,> to_array_impl(P (&a)[N], std::index_sequence) noexcept{    return { {a[I]...} };}
template<typename T, size_t N>constexpr auto to_array(T (&a)[N]) noexcept{    return to_array_impl<std::remove_cv_t, T, N>(a, std::make_index_sequence{});}
template<typename R, typename P, size_t N, size_t... I>constexpr array,> to_array_impl(P (&&a)[N], std::index_sequence) noexcept{    return { {move(a[I])...} };}
template<typename T, size_t N>constexpr auto to_array(T (&&a)[N]) noexcept{    return to_array_impl<std::remove_cv_t, T, N>(move(a), std::make_index_sequence{});}


细心的朋友会注意到,上面这个定义与C 20的推荐实现有所差异,这是有目的的。稍后我会解释这么干的原因。


现在让我们尝试下用新方法解决老问题:

auto g_cfgPara = to_array({1, 2, 5, 6, 7, 9, 3, 4});  // 类型不是uint32_t?


不对啊,为什么元素类型不是原来的std::uint32_t?


这是因为模板参数推导对std::initializer_list的元素拒绝隐式转换,如果你把to_array的模板参数从int改为uint32_t,会得到如下编译错误:

D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: error: no matching function for call to 'to_array(<brace-enclosed initializer list>)' auto g_cfgPara = to_array({1, 2, 5, 6, 7, 9, 3, 4});D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&)[N])' constexpr auto to_array(T (&a)[N]) noexcept                ^~~~~~~~D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:34:16: note:   template argument deduction/substitution failed:D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note:   mismatched types 'unsigned int' and 'int' auto g_cfgPara = to_array({1, 2, 5, 6, 7, 9, 3, 4});D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note: candidate: 'template<class T, long long unsigned int N> constexpr auto to_array(T (&&)[N])' constexpr auto to_array(T (&&a)[N]) noexcept                ^~~~~~~~D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:46:16: note:   template argument deduction/substitution failed:D:\Work\Source_Codes\MyProgram\VSCode\main.cpp:51:61: note:   mismatched types 'unsigned int' and 'int'


Hoho,有点惨是不,绕了一圈回到原点,还是不能强制指定类型。

这个时候,之前针对std::array做的修改派上用场了:我给to_array_impl增加了一个模板参数,让输入数组的元素和返回std::array的元素用不同的类型参数表示,这样就给类型转换带来了可能。为了实现转换到指定的类型,我们还需要添加两个工具函数:



template<typename R, typename P, size_t N>constexpr auto to_typed_array(P (&a)[N]) noexcept{    return to_array_impl,>(a, std::make_index_sequence{});}
template<typename R, typename P, size_t N>constexpr auto to_typed_array(P (&&a)[N]) noexcept{    return to_array_impl,>(move(a), std::make_index_sequence{});}


这两个函数和to_array的区别是:它带有3个模板参数:第一个是要返回的std::array的元素类型,后两个和to_array一样。这样我们就可以通过指定第一个参数来实现定制std::array元素类型了。

auto g_cfgPara = to_typed_array({1, 2, 5, 6, 7, 9, 3, 4});  // 自动把元素转换成uint32_t


这段代码可以编译通过和运行,但是却有类型转换的编译告警。当然,如果你胆子够大,可以在to_array_impl函数中放一个static_cast来消除告警。但是编译告警提示了我们一个不能忽视的问题:如果万一输入的数值溢出了怎么办?

auto g_a = to_typed_array({256, -1});  // 数字超出uint8_t范围


编译器还是一样的会让你编译通过和运行,g_a中的两个元素的值将分别为0和255。如果你不明白为什么这两个值和入参不一样,你该复习下整型溢出与回绕的知识了。

显然,这个方案还不完美。但我们可以继续改进。


编译期字面量数值合法性校验


首先能想到的做法是在to_array_impl函数中放入一个if判断之类的语句,对于超出目标数值范围的输入抛出异常或者做其他处理。这当然可行,但要注意的是这些工具函数是可以在运行期调用的,对于这种常用的基础函数来说,性能至关重要。一旦在里面加入了错误判断,意味着运行时的每一次调用性能都会下降。

理想的设计是:只有在编译期生成的数组才进行校验,并且报编译错误。但运行时调用函数时不要加入任何校验。

可惜的是,至少在C 20之前,没有办法指定函数只允许在编译期执行[2]。那有没有其他手段呢?

熟悉C 的人知道:C 的编译期处理大多可以用模板的trick来完成——因为模板参数一定是编译期常量。因此我们可以用模板参数来完成编译期处理——只要把数组元素全部作为模板的非类型参数就可以了。当然,这里有个问题:模板的非类型参数的类型怎么确定?正好C 17提供了auto模板参数的功能,可以派上用场:

template<typename T>constexpr void CheckIntRanges() noexcept {}  // 用于终结递归
template<typename T, auto M, auto... N>constexpr void CheckIntRanges() noexcept
  • 发表于 2020-12-15 10:25
  • 阅读 ( 723 )
  • 分类:C/C++开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1470 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章