关于数组:C语言中stdarray的神奇用法总结你需要知道

3次阅读

共计 15552 个字符,预计需要花费 39 分钟才能阅读完成。

摘要:在这篇文章里,将从各个角度介绍下 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::array
return 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::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::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 的神奇用法总结》,原文作者:飞得乐。

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

正文完
 0