我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++语言中std::array怎么用

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++语言中std::array怎么用

这篇文章给大家分享的是有关C++语言中std::array怎么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

概述

std::array是在C++11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与性能。也正因此,使得std::array有很多与其他容器不同的特殊之处,比如:std::array的元素是直接存放在实例内部,而不是在堆上分配空间;std::array的大小必须在编译期确定;std::array的构造函数、析构函数和赋值操作符都是编译器隐式声明的……这让很多用惯了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<uint32_t, 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<uint32_t> 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<R, N> to_array_impl(P (&a)[N], std::index_sequence<I...>) 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>, T, N>(a, std::make_index_sequence<N>{});}template<typename R, typename P, size_t N, size_t... I>constexpr array<R, N> to_array_impl(P (&&a)[N], std::index_sequence<I...>) 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>, T, N>(move(a), std::make_index_sequence<N>{});}

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

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

auto g_cfgPara = to_array<int>({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<uint32_t>(<brace-enclosed initializer list>)' auto g_cfgPara = to_array<uint32_t>({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<uint32_t>({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<R, P, N>(a, std::make_index_sequence<N>{});}template<typename R, typename P, size_t N>constexpr auto to_typed_array(P (&&a)[N]) noexcept{    return to_array_impl<R, P, N>(move(a), std::make_index_sequence<N>{});}

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

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

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

auto g_a = to_typed_array<uint8_t>({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{    // 防止无符号与有符号比较    static_assert(!((std::numeric_limits<T>::min() >= 0) && (M < 0)));    // 范围校验    static_assert((M >= std::numeric_limits<T>::min()) &&                   (M <= std::numeric_limits<T>::max()));     CheckIntRanges<T, N...>();}template<typename T, auto... N>constexpr auto DeclareArray() noexcept{    CheckIntRanges<T, N...>();    array<T, sizeof...(N)> a{{static_cast<T>(N)...}};    return a;};

注意这个函数中,所有的校验都通过static_assert完成。这就保证了校验一定只会发生在编译期,不会带来任何运行时开销。
DeclareArray的使用方法如下:

constexpr auto a1 = DeclareArray<uint8_t, 1, 2, 3, 4, 255>();  // 声明一个std::array<uint8_t, 5>,元素分别为1, 2, 3, 4, 255static_assert(a1.size() == 5);static_assert(a1[3] == 4);auto a2 = DeclareArray<uint8_t, 1, 2, 3, -1>();  // 编译错误,-1超出uint8_t范围auto a3 = DeclareArray<uint16_t, 1, 2, 3, 65536>();  // 编译错误,65536超出uint16_t范围

这里有一个误区需要说明:有些人可能会把DeclareArray声明成这样:

template<typename T, T... N>  // 注意N的类型为Tconstexpr auto DeclareArray() noexcept

这么做的话,会发现对数值的校验总是能通过——因为模板参数在进入校验之前就已经被转换为T类型了。如果你的编译器不支持C++17的auto模板参数,那么可以通过使用std::uint64_t、std::int64_t这些“最大”的类型来间接达到目的。
另一点要说明的是,C++对于非类型模板参数的允许类型存在限制,DeclareArray的方法只能用于数组元素为基本类型的场景(至少在C++20以前如此)。但是这也足够了。如果数组的元素是自定义类型,就可以通过自定义的构造函数等方法来控制类型转换。
如果你看到这里觉得有点意思了,那就对了,后面还有更过瘾的。

编译期生成数组

C++11中新增的constexpr修饰符可以在编译期完成很多计算工作。但是一般constexpr函数只能返回单个值,一旦你想用它返回一串对象的集合,就会遇到麻烦:STL容器都有动态内存申请功能,不能作为编译期常量(至少在C++20之前如此);而原生数组作为返回值会退化为指针,导致返回悬空的指针。即使是返回数组的引用也是不行的,会产生悬空的引用。

constexpr int* Func() noexcept{    int a[] = {1, 2, 3, 4};    return a;  // 严重错误!返回局部对象的地址}

直到std::array的出现,这个问题才得到较好解决。std::array既可以作为编译期常量,又可以作为函数返回值。于是,它成为了编译期返回集合数据的首选。
在上面to_array等工具函数的实现中,我们已经见过了编译期返回数组是怎么做的。这里我们再大胆一点,写一个编译期冒泡排序:

template<typename T, size_t N>constexpr std::array<T, N> Sort(const std::array<T, N>& numbers) noexcept{    std::array<T, N> sorted(numbers);    for (int i = 0; i < N; ++i) {        for (int j = N - 1; j > i; --j) {            if (sorted[j] < sorted[j - 1]) {                T t = sorted[j];                sorted[j] = sorted[j - 1];                sorted[j - 1] = t;            }        }    }    return sorted;}int main(){    constexpr std::array<int, 4> before{4, 2, 3, 1};    constexpr std::array<int, 4> after = Sort(before);    static_assert(after[0] == 1);    static_assert(after[1] == 2);    static_assert(after[2] == 3);    static_assert(after[3] == 4);    return 0;}

因为整个排序算法都是在编译期完成,所以我们没有必要太关注冒泡排序的效率问题。当然,只要你愿意,完全可以写出一个编译期快速排序——毕竟constexpr函数也可以在运行期使用,不好说会不会有哪个憨憨在运行时调用它。
在编写constexpr函数时,有两点需要注意:

constexpr函数中不能调用非constexpr函数。因此在交换元素时不能用std::swap,排序也不能直接调用std::sort。

传入的数组是constexpr的,因此参数类型必须加上const,也不能对数据进行就地排序,必须返回一个新的数组。

虽然限制很多,但编译期算法的好处也是巨大的:如果运算中有数组越界等未定义行为,编译将会失败。相比起运行时的测试,编译期测试constexpr函数能有效的提前拦截问题。而且只要编译通过就意味着测试通过,比起额外跑白盒测试用例方便多了。
上面的一大串static_assert语句让人看了不舒服。这么写的原因是std::array的operator==函数并非constexpr(至少在C++20前如此)。但是我们也可以自己定义一个模板函数用于判断两个数组是否相等:

template<typename T, typename U, size_t M, size_t N>constexpr bool EqualsImpl(const T& lhs, const U& rhs){    static_assert(M == N);    for (size_t i = 0; i < M; ++i) {        if (lhs[i] != rhs[i]) {            return false;        }    }    return true;}template<typename T, typename U>constexpr bool Equals(const T& lhs, const U& rhs){    return EqualsImpl<T, U, size(lhs), size(rhs)>(lhs, rhs);}template<typename T, typename U, size_t N>constexpr bool Equals(const T& lhs, const U (&rhs)[N]){    return EqualsImpl<T, const U (&)[N], size(lhs), N>(lhs, rhs);}int main(){    constexpr std::array<int, 4> before{4, 2, 3, 1};    constexpr std::array<int, 4> after = Sort(before);    static_assert(Equals(after, {1, 2, 3, 4}));  // 比较std::array和原生数组    static_assert(!Equals(before, after));  // 比较两个std::array    return 0;}

我们定义的Equals比std::array的比较运算符更强大,甚至可以在std::array和原生数组之间进行比较。
对于Equals有两点需要说明:

std::size是C++17提供的工具函数,对各种容器和数组都能返回其大小。当然,这里的Equals只会允许编译期确定大小的容器传入,否则触发编译失败。

Equals定义了两个版本,这是被C++的一个限制所逼的迫不得已:C++禁止{...}这种std::initializer_list字面量被推导为模板参数类型,因此我们必须提供一个版本声明参数类型为数组,以便{1, 2, 3, 4}这种表达式能作为参数传进去。

编译期排序是一个启发性的尝试,我们可以用类似的方法生成其他的编译期集合常量,比如指定长度的自然数序列:

template<typename T, size_t N>constexpr auto NaturalNumbers() noexcept{    array<T, N> arr{0};  // 显式初始化不能省    for (size_t i = 0; i < N; ++i) {        arr[i] = i + 1;    }    return arr;}int main(){    constexpr auto arr = NaturalNumbers<uint32_t, 5>();    static_assert(Equals(arr, {1, 2, 3, 4, 5}));    return 0;}

这段代码的编译运行都没有问题,但它并不是推荐的做法。原因是在NaturalNumbers函数中,先定义了一个内容全0的局部数组,然后再挨个修改它的值,这样没有直接返回指定值的数组效率高。有人会想能不能把arr的初始化给去掉,但这样会导致编译错误——constexpr函数中不允许定义没有初始化的局部变量。

可能有人觉得这些计算都是编译期完成的,对运行效率没影响——但是不要忘了constexpr函数也可以在运行时调用。更好的做法可以参见前面to_array函数的实现,让数组的初始化一气呵成,省掉挨个赋值的步骤。

我们用这个新思路,写一个通用的数组生成器,它可以接受一个函数对象作为参数,通过调用这个函数对象来生成数组每个元素的值。下面的代码还演示了下如何用这个生成器在编译期生成奇数序列和斐波那契数列。

template<typename T>constexpr T OddNumber(size_t i) noexcept{    return i * 2 + 1;}template<typename T>constexpr T Fibonacci(size_t i) noexcept{    if (i <= 1) {        return 1;    }    return Fibonacci<T>(i - 1) + Fibonacci<T>(i - 2);}template<typename T, size_t N, typename F, size_t... I>constexpr array<std::remove_cv_t<T>, N> GenerateArrayImpl(F f, std::index_sequence<I...>) noexcept{    return { {f(I)...} };}template<size_t N, typename F, typename T = invoke_result_t<F, size_t>>constexpr array<T, N> GenerateArray(F f) noexcept{    return GenerateArrayImpl<T, N>(f, std::make_index_sequence<N>{});}int main(){    constexpr auto oddNumbers = GenerateArray<5>(OddNumber<uint8_t>);    static_assert(Equals(oddNumbers, {1, 3, 5, 7, 9}));    constexpr auto fiboNumbers = GenerateArray<5>(Fibonacci<uint32_t>);    static_assert(Equals(fiboNumbers, {1, 1, 2, 3, 5}));    // 甚至可以传入lambda来定制要生成的数字序列(限定C++17)    constexpr auto specified = GenerateArray<3>([](size_t i) { return i + 10; });    static_assert(Equals(specified, {10, 11, 12}));    return 0;}

最后那个传入lambda来定制数组的做法存在一个疑问:lambda是constexpr函数吗?答案为:可以是,但需要C++17支持。
GenerateArray这个数组生成器将会在后面发挥重大作用,继续往下看。

截取子数组

std::array并未提供输入一个指定区间来建立新容器的构造函数,但是借助上面的数组生成器,我们可以写个辅助函数来实现子数组生成操作(这里再次用上了lambda函数作为生成算法)。

template<size_t N, typename T>constexpr auto SubArray(T&& t, size_t base) noexcept{    return GenerateArray<N>([base, t = forward<T>(t)](size_t i) { return t[base + i]; });}template<size_t N, typename T, size_t M>constexpr auto SubArray(const T (&t)[M], size_t base) noexcept{    return GenerateArray<N>([base, &t](size_t i) { return t[base + i]; });}int main(){    // 以std::initializer_list字面量为原始数据    constexpr auto x = SubArray<3>({1, 2, 3, 4, 5, 6}, 2);  // 下标为2开始,取3个元素    static_assert(Equals(x, {3, 4, 5}));    // 以std::array为原始数据    constexpr auto x1 = SubArray<2>(x, 1);  // 下标为1开始,取2个元素    static_assert(Equals(x1, {4, 5}));    // 以原生数组为原始数据    constexpr uint8_t a[] = {9, 8, 7, 6, 5};    constexpr auto y = SubArray<2>(a, 3);    static_assert(Equals(y, {6, 5}));  // 下标为3开始,取2个元素    // 以字符串为原始数据,注意生成的数组不会自动加上'\0'    constexpr const char* str = "Hello world!";    constexpr auto z = SubArray<5>(str, 6);    static_assert(Equals(z, {'w', 'o', 'r', 'l', 'd'}));  // 下标为6开始,取5个元素     // 以std::vector为原始数据,非编译期计算    vector<int32_t> v{10, 11, 12, 13, 14};    size_t n = 2;    auto d = SubArray<3>(v, n);  // 运行时生成数组    assert(Equals(d, {12, 13, 14}));  // 注意不能用static_assert,不是编译期常量    return 0;}

使用SubArray时,模板参数N是要截取的子数组大小,入参t是任意能支持下标操作的类型,入参base是截取元素的起始位置。由于std::array的大小在编译期是确定的,因此N必须是编译期常量,但参数base可以是运行时变量。

当所有入参都是编译期常量时,生成的子数组也是编译期常量。

SubArray提供了两个版本,目的也是为了让std::initializer_list字面量可以作为参数传入。

拼接多个数组

采用类似的方式可以做多个数组的拼接,这里同样用了lambda作为生成函数。

template<typename T>constexpr auto TotalLength(const T& arr) noexcept{    return size(arr);}template<typename P, typename... T>constexpr auto TotalLength(const P& p, const T&... arr) noexcept{    return size(p) + TotalLength(arr...);}template<typename T>constexpr auto PickElement(size_t i, const T& arr) noexcept{    return arr[i];}template<typename P, typename... T>constexpr auto PickElement(size_t i, const P& p, const T&... arr) noexcept{    if (i < size(p)) {        return p[i];    }    return PickElement(i - size(p), arr...);}template<typename... T>constexpr auto ConcatArrays(const T&... arr) noexcept{    return GenerateArray<TotalLength(arr...)>([&arr...](size_t i) { return PickElement(i, arr...); });}int main(){    constexpr int32_t a[] = {1, 2, 3};  // 原生数组    constexpr auto b = to_typed_array<int32_t>({4, 5, 6});  // std::array    constexpr auto c = DeclareArray<int32_t, 7, 8>();  // std::array    constexpr auto x = ConcatArrays(a, b, c);  // 把3个数组拼接在一起    static_assert(Equals(x, {1, 2, 3, 4, 5, 6, 7, 8}));    return 0;}

和之前一样,ConcatArrays使用了模板参数来同时兼容原生数组和std::array,它甚至可以接受任何编译期确定长度的自定义类型参与拼接。

ConcatArrays函数因为可变参数的语法限制,没有再对std::initializer_list字面量进行适配,这导致std::initializer_list字面量不能再直接作为参数:

constexpr auto x = ConcatArrays(a, {4, 5, 6});  // 编译错误

但是我们有办法规避这个问题:利用前面介绍过的工具把std::initializer_list先转成std::array就可以了:

constexpr auto x = ConcatArrays(a, to_array({4, 5, 6}));  // OK

编译期拼接字符串

std::array适合用来表示字符串么?回答这个问题前,我们先看看原生数组是否适合表示字符串:

char str[] = "abc";  // str数组大小为4,包括结尾的'\0'

上面是很常见的写法。由于数组名可退化为指针,str可用于各种需要字符串的场合,如传给cout打印输出。
std::array作为对原生数组的替代,自然也适合用来表示字符串。有人可能会觉得std::array没法直接作为字符串类型使用,不太方便。但实际上只要调用data方法,std::array就会返回能作为字符串使用的指针:

constexpr auto str = to_array("abc");  // to_array可以将字符串转换为std::arraystatic_assert(str.size() == 4);static_assert(Equals(str, "abc"));  // Equals也可以接受字符串字面量cout << str.data();  // 打印字符串内容

由于字符串字面量是char[]类型,因此前面所编写的工具函数,都可以将字符串作为输入参数。上面的Equals只是其中一个例子。
那之前写的数组拼接函数ConcatArrays能用于拼接字符串么?能,但结果和我们想的有差异:

constexpr auto str = ConcatArrays("abc", "def");static_assert(str.size() == 8);  // 长度不是7?static_assert(Equals(str, {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0'}));

因为每个字符串结尾都有'\0'结束符,用数组拼接方法把它们拼到一起时,中间的'\0'没有被去掉,导致结果字符串被切割为了多个C字符串。
这个问题解决起来也很容易,只要在拼接数组时把所有数组的最后一个元素('\0')去掉,并且在返回数组的末尾加上'\0'就可以了。下面的代码实现了字符串拼接功能,非类型参数E是字符串的结束符,通常为'\0',但是也允许定制。我们甚至可以利用它来拼接结束符为其他值的对象,比如消息、报文等。

// 最后一个字符,放入结束符template<auto E>constexpr auto PickChar(size_t i){    return E;}template<auto E, typename P, typename... T>constexpr auto PickChar(size_t i, const P& p, const T&... arr){    if (i < (size(p) - 1)) {        if (p[i] == E) {  // 结束符不允许出现在字符串中间            throw "terminator in the middle";        }        return p[i];    }    if (p[size(p) - 1] != E) {  // 结束符必须是最后一个字符        throw "terminator not at end";    }    return PickChar<E>(i - (size(p) - 1), arr...);}template<typename... T, auto E = '\0'>constexpr auto ConcatStrings(const T&... str){    return GenerateArray<TotalLength(str...) - sizeof...(T) + 1>([&str...](size_t i) {                return PickChar<E>(i, str...);           });}int main(){    constexpr char a[] = "I ";  // 原生数组形式的字符串    constexpr auto b = to_array("love ");  // std::array形式的字符串    constexpr auto str = ConcatStrings(a, b, "C++");  // 拼接 数组 + std::array + 字符串字面量    static_assert(Equals(str, "I love C++"));    return 0;}

这段代码中用了两个throw,这是为了校验输入的参数是否都为合法的字符串,即:字符串长度=容器长度-1。如果不符合该条件,会导致拼接结果的长度计算错误。

当编译期的计算抛出异常时,只会出现编译错误,因此只要不在运行时调用ConcatStrings,这两个throw语句不会有更多影响。但因为这个校验的存在,强烈不建议在运行期调用ConcatStrings做拼接,何况运行期也没必要用这种方法——std::string的加法操作它不香么?
有人会想:能否在编译期计算字符串的实际长度,而不是用容器的长度呢?这个方法看似可行,定义一个编译期计算字符串长度的函数确实很容易:

template<typename T, auto E = '\0'>constexpr size_t StrLen(const T& str) noexcept{    size_t i = 0;    while (str[i] != E) {        ++i;    }    return i;}constexpr const char* g_str = "abc";int main(){    // 利用StrLen把一个字符串按实际长度转成std::array    constexpr auto str = SubArray<StrLen(g_str) + 1>(g_str, 0);    static_assert(Equals(str, "abc"));    return 0;}

但是,一旦你试图把StrLen放到ConcatStrings的内部去声明数组长度,就会产生问题:C++的constexpr机制要求只有在能看到输入参数的constexpr属性的地方,才允许StrLen的返回结果确定为constexpr。而在函数内部时,看到的参数类型并不是constexpr。
当然我们可以变通一下,做出一些有趣的工具,比如使用万恶的宏:

// 把一个字符串按实际长度转成std::array#define StrToArray(x) SubArray<StrLen(x) + 1>(x, 0)constexpr const char* g_str = "abc";int main(){    // 使用宏,可以让constexpr指针类型也参与编译期字符串的拼接    constexpr auto str = ConcatStrings(StrToArray(g_str), "def");    static_assert(Equals(str, "abcdef"));    return 0;}

使用宏以后,ConcatStrings连编译期不确定大小的指针类型都可以间接作为输入了[3]。如果你狠得下心使用变参宏,甚至可以定义出按实际字符串长度计算结果数组长度的更通用拼接函数。但我严重怀疑这种需求的必要性——毕竟我们只是做编译期的拼接,而编译期的字符串不应该会有结束符位置不在末尾的场景。
看到这里的人,或多或少该佩服一下std::array的强大了。上面这些编译期操作,用原生数组很难完成吧?

展望C++20——打破更多的枷锁

我在文章中说了多少次“至少在C++20之前如此”?不记得了,但是能确定的是:C++20会带来很多美好的东西:std::array会有constexpr版本的比较运算符;函数可以用consteval限定只在编译期调用;模板非类型参数允许更多的类型;STL容器对象可以作为constexpr常量……所有这一切,都只是C++20的minor更新而已,在绝大多数的特性介绍中,它们连提都不会被提到!

可想而知,用上C++20以后,编程会发生多大的变化。那时我们再来找找更多有趣的用法

尾注

[1]to_array定义了两个版本,分别以左值引用和右值引用作为参数类型。按照C++11的最优实践,这样的函数本应该只定义一个版本并且使用完美转发。但是to_array的场景如果用万能引用会带来一个问题:C++禁止std::initializer_list字面量{...}被推导为模板类型参数,完美转发方案会导致std::initializer_list字面量不能作为to_array的入参。在后面内容中我们会看到多次这个限制所带来的影响。

[2]C++20加入了consteval修饰符,可以指定函数只允许在编译期调用。

[3] 需要注意的是:constexpr用于修饰指针时,表示的是指针本身为常量(而不是其指向的对象)。和const不同,constexpr并不允许放在类型声明表达式的中间。因此如果要在编译期计算一个constexpr指针指向的字符串长度,这个字符串必须位于静态数据区里,不能位于栈或者堆上(否则其地址无法在编译期确定)。

感谢各位的阅读!关于“C++语言中std::array怎么用”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++语言中std::array怎么用

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C++语言中std::array怎么用

这篇文章给大家分享的是有关C++语言中std::array怎么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。概述std::array是在C++11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与
2023-06-15

c++中std::怎么用

std 是 c++ 中包含标准库组件的命名空间。为了使用 std,需要使用 "using namespace std;" 语句。直接使用 std 命名空间中的符号可以简化代码,但建议仅在需要时使用,以避免命名空间污染。std 在 C++ 中
c++中std::怎么用
2024-05-09

C++中std::thread线程怎么使用

本篇内容主要讲解“C++中std::thread线程怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++中std::thread线程怎么使用”吧!1:std::thread的基本用法最简
2023-07-04

R语言中vector向量,array数组怎么用

这篇文章将为大家详细讲解有关R语言中vector向量,array数组怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。R语言很奇怪的是它是面向对象的语言,所以经常会调用系统的方法,而且更奇怪的是总是调用
2023-06-08

C++ std::thread怎么使用

这篇文章主要介绍了C++ std::thread怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++ std::thread怎么使用文章都会有所收获,下面我们一起来看看吧。C++是一种高级编程语言,被广
2023-07-05

C++中多线程std::call_once怎么用

这篇文章主要介绍了C++中多线程std::call_once怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。在多线程的环境下,有些时候我们不需要某个函数被调用多次或者某些
2023-06-29

c++中std::什么作用

std:: 是 c++ 中包含标准库函数、类和对象的命名空间,简化了软件开发。其具体作用包括:提供数据结构容器,如向量和集合;提供遍历容器的迭代器;包含各种算法用于操作数据;提供输入/输出流对象用于处理 i/o 操作;提供其他实用工具,如异
c++中std::什么作用
2024-05-09

C++20中的std::span怎么使用

这篇文章主要讲解了“C++20中的std::span怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++20中的std::span怎么使用”吧!span就是一个连续对象存储的观察者。
2023-07-05

C++的std::any怎么使用

这篇文章主要介绍了C++的std::any怎么使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++的std::any怎么使用文章都会有所收获,下面我们一起来看看吧。 一般来说,c++是一种具有类型绑定
2023-06-29

C++11中std::ref和std::cref的作用是什么

这篇文章给大家分享的是有关C++11中std::ref和std::cref的作用是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。1、源码准备本文是基于gcc-4.9.0的源代码进行分析,std::ref和st
2023-06-15

C++中STL标准库std::vector怎么用

小编给大家分享一下C++中STL标准库std::vector怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1. 简介vector 是表示可以改变大小的数组的
2023-06-29

c语言中strcpy怎么用

strcpy 函数在 c 中复制字符串,使用方法如下:声明目标和源字符串数组。调用 strcpy 函数,将源字符串复制到目标字符串。注意目标字符串数组必须足够大,且 strcpy 不会添加空字符,需要手动添加。替代函数:在 c11 及更高版
c语言中strcpy怎么用
2024-05-21

c语言中bool怎么用

bool 类型是一种基本数据类型,用于存储布尔值(真或假),它与 int 类型类似,但仅能存储两个特定值:真 (true) 或假 (false),分别表示 1 和 0。它支持比较运算符和逻辑运算符,用于控制流语句、存储逻辑状态和创建布尔表达
c语言中bool怎么用
2024-04-29

c语言中default怎么用

default 是 c 语言 switch 语句中的关键字,用于指定当没有匹配的分支时执行的代码块:语法:switch (表达式) { case 值1: 代码块 1; break; case 值2: 代码块 2; break; ... de
c语言中default怎么用
2024-05-02

c语言中continue怎么用

c 语言中 continue 语句的功能是跳过循环的剩余部分,直接继续执行下一轮循环。用法:1. 只能在循环结构中使用;2. 语句放置在需要跳过的位置;3. 效果是跳过剩余语句,转到下一个迭代。C 语言中 continue 的用法cont
c语言中continue怎么用
2024-05-02

c语言中float怎么用

在 c 语言中,float 数据类型用于表示浮点数字:声明变量:float myfloat;赋值:myfloat = 3.1415;使用:float result = myfloat * 2;需要注意的是,float 是 32 位浮点数,可
c语言中float怎么用
2024-05-21

C语言中auto怎么用

这篇文章主要为大家展示了“C语言中auto怎么用”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“C语言中auto怎么用”这篇文章吧。你以为我说的自动变量类型auto吗?非也,我们知道C语言中其实也
2023-06-16

C语言中typedef怎么用

这篇文章主要介绍了C语言中typedef怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1、简洁定义C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。而编程
2023-06-20

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录