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