一、从一个例子开始
#include <iostream>
#include <array>
#include <type_traits>
//这个例程必须在c++17及以上
template <size_t ...N>
static constexpr auto Square(size_t index, std::index_sequence<N ...>)
{
//如果在c++17之下,下面会报常量初始化错误,注意下面这个省略号的应用
constexpr auto nums = std::array{ N * N ... };
return nums[index];
}
template <size_t N>
constexpr static auto BuildArray(size_t index)
{
//生成初始化数组的向量,调用初始化的目的是为了展开Square模板中的N,也就那个...
return Square(index, std::make_index_sequence<N>{});
}
int main()
{
static_assert(BuildArray<16>(3) == 3 * 3);
auto b = BuildArray<31>(20) == 20 * 20;
std::cout << "bool:" << b << std::endl;
}
这是一个很简单的例子,用make_index_sequence展开来得到数组,并验证得到的数组是不是N的平方,验证的方式其实就是通过std::index_sequence得到具体的索引来进行。例子简单,但里面这个STL中的make_index_sequence相关的应用是如何得来的,却是让新人头脑有点转不过来。下面就对期进行分析,而分析用到的这些知识,在前面都已经讲过了。
二、make_index_sequence的分析说明
在上面的引入例子里,std::make_index_sequence{},使用大括号进行初始化,这个在以前的分析中也说明过,它的作用主要就是得到模板中的具体的实例,也就是{0,1,2,…N},为了更清楚的看明白,可以把编译器的版本回退到c++14,则报的错误中有下面的场景:
“查看对正在编译的函数 模板 实例化“auto Square<0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15>(size_t,std::integer_sequence<size_t,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15>)”的引用”。
是不是就明白了,所以有一个好用的编译器工具非常省时间省心思。而后面的std::array{ N * N … }不过是对展开参数的一种应用。下面看一下make_index_sequence的定义:
template< class T, T... Ints >
struct integer_sequence;
模板形参
T - 用于序列元素的整数类型
...Ints - 表示序列的非类型形参包
而为了辅助实现这个类,又定义了下面的几个辅助类:
为帮助 T 为 std::size_t 的常用情况,定义辅助别名模板 std::index_sequence 。
template<std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
分别定义辅助别名模板 std::make_integer_sequence 与 std::make_index_sequence 以简化以 0, 1, 2, ..., N-1 为 Ints 创建 std::integer_sequence 与 std::index_sequence 类型:
template<class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = std::make_integer_sequence<std::size_t, N>;
若 N 为负则程序为谬构。若 N 为零,则指示类型为 integer_sequence<T> 。
定义辅助类模板 std::index_sequence_for ,以转换任何类型参数包为同长度的下标序列:
template<class... T>
using index_sequence_for = std::make_index_sequence<sizeof...(T)>;
通过上述的定义明白了,make_index_sequence只是integer_sequence的一种特化。其实就是一种因为特化不同导致的名字变来变去,一定要看清楚。弄明白了这个,就可以想到std::tuple的访问了,一般来说访问一个Tuple,会用Get依次获取值即可。但如果用make_index_sequence则会有不错的效果来处理这些情况,看下面例程:
template <typename Tuple, typename Func, size_t ... N>
void TupleCall(const Tuple& t, Func&& func, std::index_sequence<N...>) {
static_cast<void>(std::initializer_list<int>{(func(std::get<N>(t)), 0)...});
}
template <typename ... Args, typename Func>
void TupleTravel(const std::tuple<Args...>& t, Func&& func) {
TupleCall(t, std::forward<Func>(func), std::make_index_sequence<sizeof...(Args)>{});
}
void test()
{
auto t = std::make_tuple(1, 3.68, "how are you");
TupleTravel(t, [](auto&& au) {
std::cout << au << ","<<std::endl;
});
}
int main()
{
test();
return 0;
}
有了开始的代码引入,再理解这个就好理解了,其实就是通过make_index_sequence展开通过sizeof …(Args)得到变参数量,std::index_sequence得其索引,利用std::get得到具体的数据,注意这个std::initializer_list的用法,在以前讲变参模板时,就有这种方法,用0的目的是这个数据初始化没有啥用处。
其实在c++17中,也是利用这个原理来实现了std::apply,std::tuple就好搞多了,在前面的《c++17中的apply和make_from_tuple》也有说明。
三、编程实现
既然有珠玉在前,咱们就试着也按思路实现一下:
#include <iostream>
//自定义整型序列,不要和STL中的index_sequence 混淆
template<int...Ids>
struct IdSeq {};
//继承并展开参数包
template<int N, int... Ids>
struct MakeIds : MakeIds<N - 1, N - 1, Ids...> {};
// 通过模板特化,终止展开包,想想c++17中的折叠表达式
template<int... Ids>
struct MakeIds<0, Ids...>
{
//typedef IdSeq<Ids...> type;
//using type = IdSeq<Ids ...>;
typename typedef IdSeq<Ids ...> type;
};
int main()
{
using T = MakeIds<9>::type;
std::cout << typeid(T).name() << std::endl;
return 0;
}
看了上面的例程,再看下面的代码就好理解了:
template<int... N>
struct index_seq {};
template<int N, int ...M>
struct make_index_seq : public make_index_seq<N - 1, N - 1, M...> {
};
template<int ...M>
struct make_index_seq<0, M...> : public index_seq<M...> {};
网上还有一个尾插的实现:
template<size_t ... Args>
struct index_sequence {};
template<typename T, size_t newValue>
struct AppendNewValue;
template<size_t... Args, size_t newValue>
struct AppendNewValue<index_sequence<Args...>, newValue> {
using type = index_sequence<Args..., newValue>;
};
template<size_t N>
struct make_index_sequence
{
using type = typename AppendNewValue<typename make_index_sequence<N - 1>::type, N - 1>::type;
};
template<>
struct make_index_sequence<0>
{
using type = index_sequence<>;
};
int main()
{
using T2 = make_index_sequence<9>::type;
std::cout << typeid(T2).name() << std::endl;
return 0;
}
注意,上面的make_index_sequence和index_sequence 都没有std::,这是和STL中的相关的区别开来,为了安全,可以增加自己的名空间。
无论上头插实现还是尾插实现,都配合着看一下代码,就没有了秘密可言。
在StackOverFlow上https://stackoverflow.com/questions/49669958/details-of-stdmake-index-sequence-and-stdindex-sequence上有这个讨论的实现和说明下面只把代码贴上来,有兴趣可以和上面的对比一下:
#include <cstddef>
namespace A
{
template <std::size_t... Ns>
struct index_sequence {};
template <std::size_t N, std::size_t... Is>
auto make_index_sequence_impl() {
// only one branch is considered. The other may be ill-formed
if constexpr (N == 0) return index_sequence<Is...>(); // end case
else return make_index_sequence_impl<N - 1, N - 1, Is...>(); // recursion
}
template <std::size_t N>
using make_index_sequence = std::decay_t<decltype(make_index_sequence_impl<N>())>;
};
///
namespace B
{
template <std::size_t N, std::size_t ...I>
constexpr auto make_index_sequence_impl() noexcept
{
if constexpr (!N)
{
return std::index_sequence<I...>();
}
else if constexpr (!sizeof...(I))
{
return make_index_sequence_impl<N - 1, 0>();
}
else if constexpr (N >= sizeof...(I))
{
return make_index_sequence_impl<N - sizeof...(I), I..., sizeof...(I) + I...>();
}
else
{
return[]<auto ...J>(std::index_sequence<J...>) noexcept
{
return std::index_sequence<I..., sizeof...(I) + J...>();
}(make_index_sequence_impl<N>()); // index concatenation
}
}
template <size_t N>
using make_index_sequence = decltype(make_index_sequence_impl<N>());
};
有一个国内转载的汉化地址:https://www.likecs.com/ask-882834.html#sc=400,同时网上有一个源码分析,类似于这个尾插法实现的,有兴趣可以去搜一下。
四、总结
本来一直觉得这个std::make_index_sequence没有什么可讲的,可在往后分析元编程时发现,这个还挺有代表性,所以就拿出来时间把它专门整理了一下,其实越分析,越往后学习,就会发现,其实这些基础的东西组合起来,就可以发挥出一些令人拍案的用法。
这和区块链一样,区块链本身并没有什么创新的技术,只是多种技术的工程化组合,就创造出了一个新兴的领域。以此为借鉴吧,努力!