目前进入 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

而借助这些新的特性,以后可以直接写出这样的代码:

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++ 模板元编程焕然一新。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.