摘要:在这篇文章里,将从各个角度介绍下std::array的用法,心愿能带来一些启发。

td::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:WorkSource_CodesMyProgramVSCodemain.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:WorkSource_CodesMyProgramVSCodemain.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:WorkSource_CodesMyProgramVSCodemain.cpp:34:16: note: template argument deduction/substitution failed:
D:WorkSource_CodesMyProgramVSCodemain.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:WorkSource_CodesMyProgramVSCodemain.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:WorkSource_CodesMyProgramVSCodemain.cpp:46:16: note: template argument deduction/substitution failed:
D:WorkSource_CodesMyProgramVSCodemain.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, 255
static_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的类型为T
constexpr 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函数时,有两点须要留神:

1. constexpr函数中不能调用非constexpr函数。因而在替换元素时不能用std::swap,排序也不能间接调用std::sort

  1. 传入的数组是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::arrayreturn 0;

}

咱们定义的Equals比std::array的比拟运算符更弱小,甚至能够在std::array和原生数组之间进行比拟。
对于Equals有两点须要阐明:

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

2. 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) { 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 { 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... { 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::arrayconstexpr auto c = DeclareArray<int32_t, 7, 8>();  // std::arrayconstexpr 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::array
static_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... { 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的神奇用法总结》,原文作者:飞得乐。

点击关注,第一工夫理解华为云陈腐技术~