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 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

C 语言中std::array的神奇用法总结相关推荐

  1. C++语言中std::array的神奇用法总结,你需要知道!

    摘要:在这篇文章里,将从各个角度介绍下std::array的用法,希望能带来一些启发. td::array是在C++11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与性能.也正因此, ...

  2. c语言常量的正确表示const,C语言中的const和free用法详解

    注意:C语言中的const和C++中的const是有区别的,而且在使用VS编译测试的时候.如果是C的话,请一定要建立一个后缀为C的文件,不要是CPP的文件.因为,两个编译器会有差别的. 一.C语言中的 ...

  3. c语言中if和goto的用法,C语言中if和goto的用法.doc

    C语言中if和goto的用法 C语言中,if是一个条件语句,用法??if(条件表达式) 语句如果满足括号里面表达式,表示逻辑为真于是执行后面的语句,否则不执行(表达式为真则此表达式的值不为0,为假则为 ...

  4. go语言中error的分类与用法

    go语言中error的分类与用法 原文引用:极客时间中的课程<Go error处理最佳实践> 前言:本文要讨论的就是go中error的基本原理/类型,以及最重要的几个问题: go代码开发中 ...

  5. 在c语言中while与do-while,C语言中while /do while语句用法

    C语言中while /do while语句用法 C语言while语句的用法 while语句的一般形式为:while(表达式)语句 其中表达式是循环条件,语句为循环体. while语句的语义是:计算表达 ...

  6. c语言while break用法举例,c语言中continue和break的用法

    目前,随着计算机在人们生活和工作中的普及,其教学研究地位也在逐渐提升.C语言是一种计算机程序设计语言,其具有高级语言和汇编语言的特点.下面小编就跟你们详细介绍下c语言中continue和break的用 ...

  7. c语言do while语句用法6,C语言中while /do while语句用法

    摘要 腾兴网为您分享:C语言中while /do while语句用法,仙乐,同程旅游,天猫超市,闪送等软件知识,以及上网本系统,酷我音乐mac,美版微信,地基承载力计算,云解压,猫咪咖啡馆游戏,智课雅 ...

  8. c语言中std::map_在现代C ++中明智地使用std :: map

    c语言中std::map std::map and its siblings(std::multimap, std::unordered_map/multimap) used to be my fav ...

  9. c++语言中ifndef和endif的用法

    1.#ifndef "if not defined"的简写,是宏定义的一种,它是可以根据是否已经定义了一个变量来进行分支选择,一般用于调试等.实际上确切的说这应该是预处理功能中三种 ...

最新文章

  1. 实现 连续15签到记录_MySQL和Redis实现用户签到,你喜欢怎么实现?
  2. 【C#】MD5 小程序编写
  3. c++中可以对类中私有成员中的静态变量初始化吗?
  4. 公共的service接口
  5. MobileGestalt.h 头文件
  6. P6775-[NOI2020]制作菜品【贪心,dp】
  7. session的removeattribute移除一个不存在的属性会怎么用_公认峡谷机制最完美,对线几乎无解,夏侯惇高端局为何火不起来?...
  8. [Algorithm]一切始于ADT-表达式计算
  9. Android-activity之间的交流
  10. python里的平方_python中平方和
  11. 分离整数的各个数位的两种方法
  12. 差错控制 —— 码距和纠错编码(海明码)
  13. word中页眉页脚问题处理方法
  14. 万物互联之~网络编程基础篇
  15. 【C4D周练作业031-040】周练作业渲染了个奥特曼哈哈~
  16. 非线性微分方程的平均法
  17. 布考斯基样样干_没有酒,我就是个无趣的人——查尔斯·布考斯基
  18. BIOS追code之PEI phase
  19. JavaScript压缩工具JSA使用介绍
  20. moviepy音视频开发:音频合成类CompositeAudioClip介绍

热门文章

  1. cargo maven_与Maven 3,Failsafe和Cargo插件的集成测试
  2. apache camel_探索Apache Camel Core –文件组件
  3. 带有Spring Boot 2.0的Spring Security:UserDetailsS​​ervice
  4. apache jmeter_Apache Server和JMeter调试
  5. java原子更新类_Java内部具有原子更新的动态热交换环境
  6. 我们如何意外地将Hibernate的JDBC流量增加了一倍
  7. 带有JAX-WS和Spring的Web服务应用程序
  8. Java A的新本地变量类型推断
  9. 如何:带有Thymeleaf和Spring Boot的Java 8日期和时间
  10. 关于Servlet和异步Servlet