目前进入 C++26 的特性当中,Pack Indexing 是较为有用的一个,值得谈谈。
发展背景
早期,C++ 元编程是摸着石头过河,许多特性只是当时情况下的权宜之计,并非最理想的解决方式。纵然非常巧妙,却也治标而不治本,诸多简单功能,写来亦是繁琐不已。
扬汤止沸,莫若去薪。不断向下一阶段发展的元编程,就是要彻底解决早期妥协所留下的问题,提供最优雅的解决方式,摆脱奇技淫巧带来的复杂性。
Pack Indexing 就是在这种背景下所诞生的一个新特性,提了多年,终于进了 C++26。
在此之前,C++ 就有一些与参数包相关的增强特性,比如 C++17 Fold expressions 和 Using-declarations,C++20 Lambda capture,还有原本打算进入 C++23 却一直悬而未决的 Expansion statements(最近被人重拾,兴许会入 C++26)。
此间,也有一些处于发展中的其他特性,比如 Pack declarations、Pack slicing 和 Pack literals,Pack Indexing 就是其中之一,它最先进了标准。
新的索引式访问方式
当前,若要定义一个参数包变量,我们需要借助 std::tuple
;若要索引式访问参数包元素,需要借助 std::get
和 std::tuple_element
;若要解包,需要借助 std::apply
。
而借助这些新的特性,以后可以直接写出这样的代码:
1template <typename... Ts>
2class Tuple {
3public:
4 constexpr Tuple(Ts&&... ts)
5 : elems(std::forward<Ts>(ts))...
6 { }
7
8 template <size_t I>
9 auto get() const& -> Ts...[I] const& {
10 return elems...[I]; // pack indexing
11 }
12
13private:
14 Ts... elems; // variable packs
15};
16
17template <size_t I, typename... Ts>
18struct std::tuple_element<I, Tuple<Ts...>>
19{
20 using type = Ts...[I]; // pack indexing
21};
22
23int main() {
24 Tuple<int, char> tup(1, 'c');
25 return tup.get<0>();
26}
这种实现 tuple
的方式借助了 Pack indexing 和 Variable packs(尚未入标准),它比 Reflection for C++26 中介绍的反射方式还要直接了当,是最简洁的实现方式。
归根到底,其他方式都没有釜底抽薪地解决根本问题,实现起来需要借助诸多技巧,非常麻烦。对于这此麻烦的方式,不应习以为常,也不应会点奇技淫巧就忽略了真正的问题。这是 C++ 历史的局限,最初就应该是这种直接了当的设计。
语法解析
深思熟虑过后,最终 Pack Indexing 的语法为:
1name-of-a-pack ... [constant-expression]
这使得我们可以直接访问指定位置的参数包,例子:
1template <typename... T>
2constexpr auto first_plus_last(T... values) -> T...[0] {
3 return T...[0](values...[0] + values...[sizeof...(values)-1]);
4}
5
6static_assert(first_plus_last(1, 2, 10) == 11);
T...[N]
针对的是 Types,而 values...[N]
针对的是 Values。参数包的首位元素和末位元素被相加起来,返回一个编译期常量值。
尚未完善
虽然 Pack Indexing 已进 C++26,但当前还未非常完善。
比如不支持 From-the-end-indexing,原本想用负数索引来表示从后向前访问,但可能会存在问题。看如下例子:
1// Return the index of the first type convertible to Needle in Pack
2// or -1 if Pack does not contain a suitable type.
3template <typename Needle, typename... Pack>
4auto find_convertible_in_pack;
5
6// if find_convertible_in_pack<Foo, Types...> is -1, T will be the last type, erroneously.
7using T = Types...[find_convertible_in_pack<Foo, Types...>];
find_convertible_in_pack
返回值若为 -1,则会导致语义错误。
后面会解决这个问题,或是采用其他的语法形式,比如:
1using Bar = T...[^1]; // C#. first from the end
2using Bar = T...[$ - 1]; // Dlang. first from the end
支持容易,关键是要全面考虑潜在的问题。
还有下面这种简化语法尚不支持:
1void g(auto&&);
2
3template <typename...T>
4void f(T&&... t) {
5 g(std::forward<T...[0]>(t...[0])); // current proposal
6 g(std::forward<T>(t)...[0]); // not proposed nor implemented
7}
还有其他潜在冲突,在此不一一列举,更加完善之后,续写一篇详述。
未来走向
Pack Indexing 只是向前走出了一小步,还有其他相关特性与其相辅相成,只有它们都进入标准,才能真正简化参数包的相关操作。
例如,variable packs,允许直接定义一个参数包变量。
1template <typename... Ts>
2struct S {
3 Ts... packs;
4};
再如,Adding a layer of packness,允许将 tuple-like type 转换为 packs。
1void f(std::tuple<int, char, double> t) {
2 // equivalent to g(std::get<0>(t), std::get<1>(t), std::get<2>(t))
3 // or, possibly, std::apply(g, t)
4 g(t.[:]...);
5
6 // decltype(u) is the same as T - just a really complex way to
7 // get there
8 using T = decltype(t);
9 std::tuple<T::[:]...> u = t;
10}
v.[I]
和 T::[I]
分别是 v.[:]...[I]
和 T::[:]...[I]
的语法糖,省略索引,则相当于指定所有元素。
又如,Pack slicing,再以上特性基础上,允许指定索引范围。
1void h(std::tuple<int, char, double> t) {
2 // a is a tuple<int, char, double>
3 auto a = std::tuple(t.[:]...);
4
5 // b is a tuple<char, double>
6 auto b = std::tuple(t.[1:]...);
7
8 // c is a tuple<int, char>
9 auto c = std::tuple(t.[:-1]...);
10
11 // d is a tuple<char>
12 auto d = std::tuple(t.[1:2]...);
13}
还有 Pack literals,允许直接创建一个参数包。
1// b is a pack of int's
2auto... b = { 1, 2, 3 };
可以用来为参数包设置默认参数:
1template <typename... Ts = ...<int>>
2void foo(Ts... ts = ...{0});
3
4foo(); // calls foo<int>(0);
……
总结
访问参数包的方式很多,这些方式虽然巧妙,却并不是最佳方案,只是临时解决问题,未将问题彻底解决。Pack Indexing 则是真正解决了这个问题,是最优雅的方式。
通过这种方式,参数包的访问就像数组的访问,直接了当,清晰易懂,极大简化了可变模板参数的操作。
希望其他相关特性紧随其后,完善这部分空缺,慢慢淘汰旧时期妥协的产物,使 C++ 模板元编程尽早焕然一新。