c++26新功能——Pack indexing
一、模板编程
在模板编程中,有一个问题比较突出,就是对变参模板中参数的控制,比较麻烦。因为是变参,所以想把参数单独拿出来处理,就需要借助一些特殊的技巧,而这种特殊的技巧,往往为大多数开发者所难于掌握。从而进一步加强了开发者对C++的主动或被动的疏离。但这确不是C++标准制定者们希望看到的,所以发现问题就解决问题。即使问题多,慢慢来,总会解决的。
而Pack indexing就是解决这种变参参数包(变参的集合)拆分的一个非常实用的方法,它可以通过类似数组访问的方式进行参数包中的每个参数的访问,有点类似于std::tuple中的数据获取的意思。可以将其直接翻译成包索引。
二、应用和实例
在应用时,它一般有两种形态,一种是包索引表达式;另外一种是包索引说明符。但无论哪种形式,其必须如数组的索引一般,是一个常量。
看一下最基础的的代码应用:
template<int Id, typename... Ts>
auto getIdv( Ts... vals )
{return [...args = vals]() {return args...[Id];};
}int main()
{auto a = getIdv<0>( 3, 6, 8.8 );auto b = getIdv<1>( 1.1, 6, 10.0f );return 0;
}
也可以看一看一篇文章中的Pack indexing的例子,那个例子更简单。
包索引表达式支持常量模板开参包、函数形参包、Lambda初始化捕获包以及结构绑定的包,参看下面的代码:
template <std::size_t I, typename... Ts>
constexpr auto element_at(Ts... args)
{// 'args' 在函数形参包声明中引入return args...[I];
}static_assert(element_at<0>(3, 5, 9) == 3);
static_assert(element_at<2>(3, 5, 9) == 9);
static_assert(element_at<3>(3, 5, 9) == 4); // 错误:超出范围
static_assert(element_at<0>() == 1); // 错误:超出范围,空包template <std::size_t I, typename Tup>
constexpr auto structured_binding_element_at(Tup tup)
{auto [...elems] = tup;// 'elems' 在结构化绑定包的声明中引入return elems...[I];
}struct A { bool a; int b; };static_assert(structured_binding_element_at<0>(A {true, 4}) == true);
static_assert(structured_binding_element_at<1>(A {true, 4}) == 4);// 'Vals' 在常量模板形参包声明中引入
template <std::size_t I, std::size_t... Vals>
constexpr std::size_t double_at = Vals...[I] * 2; // OKtemplate <std::size_t I, typename... Args>
constexpr auto foo(Args... args)
{return [...members = args](Args...[I] op){// 'members' 在 lambda 初始化捕获包中引入return members...[I] + op;};
}static_assert(foo<0>(4, "Hello", true)(5) == 9);
static_assert(foo<1>(3, std::string("C++"))("26") == "C++26");
而包索引说明符类似于重定义表达式即需要计算类型说明符,其typedef 名 应由类型模板形参包的声明引入:
typedef-name ...[ expression ]
看一下相关的例程:
template <typename... Ts>
using last_type_t = Ts...[sizeof...(Ts) - 1];static_assert(std::is_same_v<last_type_t<>, int>); // 错误:超出范围
static_assert(std::is_same_v<last_type_t<int>, int>);
static_assert(std::is_same_v<last_type_t<bool, char>, char>);
static_assert(std::is_same_v<last_type_t<float, int, bool*>, bool*>);
最后再看一个例程:
#include <tuple>template <std::size_t... Indices, typename Decomposable>
constexpr auto splice(Decomposable d)
{auto [...elems] = d;return std::make_tuple(elems...[Indices]...);
}struct Point
{int x;int y;int z;
};int main()
{constexpr Point p { .x = 1, .y = 4, .z = 3 };static_assert(splice<2, 1, 0>(p) == std::make_tuple(3, 4, 1));static_assert(splice<1, 1, 0, 0>(p) == std::make_tuple(4, 4, 1, 1));
}
三、目前存在的问题和未来的发展
使用Pack indexing虽然方便,但目前仍然存在着不少的问题,首先包索引不支持模板的模板参数展开后的索引(即模板的参数仍然是模板的情况下)另外一个是无法进行反向索引,即从末尾向前推进(使用负数向前推进);还有就是对万能引用这种参数包的情况下支持不够以及不支持复杂表达式的索引访问等等,细节的问题还有不少,大家暂时不要想当然的使用这个技术,看一个简单的例子:
template <std::size_t I, auto... Vals>
constexpr auto identity_at = (Vals)...[I]; // 错误
// 使用 'Vals...[I]' 代替template <std::size_t I, std::size_t... Vals>
constexpr std::size_t triple_at = (Vals * 3)...[I]; // 错误
// 使用 'Vals...[I] * 3' 代替template <std::size_t I, typename... Args>
constexpr decltype(auto) get(Args&&... args) noexcept
{return std::forward<Args>(args)...[I]; // 错误// 使用 'std::forward<Args...[I]>(args...[I])' 代替
}
另外还有对指针的处理和对一些关键字的应用等等,都不能掉以轻心,需要严格的按照相关文档进行应用。
包索引技术属于新技术初期经历的探索阶段,总之这个技术目前看还在发展过程中,不过发展的方向是不错的。
四、Pack indexing应用前的参数获取
这里给出一个常用的在不使用Pack indexing情况下访问模板变参包的相关参数的方法:
template<int Idx, typename T, typename... Ts>
static constexpr auto getVal( T val, Ts... vals )
{if constexpr ( Idx == 0 ){return val;}else {return getVal<Idx - 1>( vals... );}
}template<int Idx, typename... Ts>
struct getType;template<typename T, typename... Ts>
struct getType<0, T, Ts...>
{using type = T;
};template<int Idx, typename T, typename... Ts>
struct getType<Idx, T, Ts...>
{using type = typename getType<Idx - 1, Ts...>::type;
};template<int Index, typename... Ts>
void Test( Ts... args )
{using type = typename getType<Index, Ts...>::type;auto val = getVal<Index>( args... );
}int main()
{Test<0>( 1, 2.2, 3 );Test<1>( 2, 3.3, 4 );Test<2>( 3, 4.4, 5 );
}
这个和前面的std::make_index_sequence展开和tuple索引访问的实现有异曲同工的效果。把这种方法和Pack indexing的方式进行比较,哪种简单易用,不容易出问题就立刻有了结果。所以说,标准的进步,一定是好事。
五、总结
一个技术的出现也会有生命周期中的几个阶段,一般来说只有到了成熟期才会非常广泛的应用于开发者的开发过程中。毕竟大多数的开发者和其服务的公司,都不愿意承担学习和应用的风险。但如果这项技术已经被验证是一个未来,还是应该努力的从开始就跟进它,从而更好的掌握这门技术,能更好的服务于自己的开发实践!