C++26 Pack Indexing

科技   2024-04-02 09:23   浙江  

目前进入 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::getstd::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<intchar> 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(1210) == 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<intchardouble> 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<intchardouble> 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 = { 123 };

可以用来为参数包设置默认参数:

1template <typename... Ts = ...<int>>
2void foo(Ts... ts = ...{0});
3
4foo(); // calls foo<int>(0);

……

总结

访问参数包的方式很多,这些方式虽然巧妙,却并不是最佳方案,只是临时解决问题,未将问题彻底解决。Pack Indexing 则是真正解决了这个问题,是最优雅的方式。

通过这种方式,参数包的访问就像数组的访问,直接了当,清晰易懂,极大简化了可变模板参数的操作。

希望其他相关特性紧随其后,完善这部分空缺,慢慢淘汰旧时期妥协的产物,使 C++ 模板元编程尽早焕然一新。

推荐阅读  点击标题可跳转

1、libc.so.6小入门

2、C++之父反驳白宫,称拜登政府忽视了现代C++编程语言的优势

3、回调函数(callback)是什么?一文理解回调函数(callback)

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章