相关文章系列
std::apply源码分析
C++之std::tuple(一) : 使用精讲(全)
目录
1.std::integer_sequence
2.std::index_sequence
3.std::make_index_sequence
4.运用
4.1.打印序列的值
4.2.编译时求值
4.3.std::tuple访问值
5.总结
1.std::integer_sequence
运行时定义一个随机序列比较简单,那么编译时怎么表示一个随机序列怎么办呢?于是std::interger_sequence产生了,它的作用就是表示一个integral数据的序列,这里的integral数据代表std::is_integral为true的数据类型,它主要包括下图列举的一些数据:
std::interger_sequence的定义如下:
template <class _Ty, _Ty... _Vals>
struct integer_sequence { // sequence of integer parameters
static_assert(is_integral_v<_Ty>, "integer_sequence<T, I...> requires T to be an integral type.");
using value_type = _Ty;
_NODISCARD static constexpr size_t size() noexcept {
return sizeof...(_Vals);
}
};
通过static_assert和std::is_integral_v限制数据的类型为integral, 通过constexpr编译时求序列的大小。
2.std::index_sequence
它定义如下:
template <size_t... _Vals>
using index_sequence = integer_sequence<size_t, _Vals...>;
它是一个类模板,表示数据类型为std::size_t的一个序列。它在模板元编程中用于编译时迭代。
3.std::make_index_sequence
它的定义如下:
template <class _Ty, _Ty _Size>
using make_integer_sequence = __make_integer_seq<integer_sequence, _Ty, _Size>;
template <size_t _Size>
using make_index_sequence = make_integer_sequence<size_t, _Size>;
template <class... _Types>
using index_sequence_for = make_index_sequence<sizeof...(_Types)>;
从中可以看出std::make_index_sequence
是一个模板别名,它生成一个std::index_sequence
类型的对象,该对象包含一系列递增的整数。这个工具在编译时生成一系列的索引,常常用于元编程和编译时计算。
这里,std::size_t _size是一个模板参数,表示生成的序列的大小。std::make_integer_sequence是一个模板,它生成一个包含从0到N-1的整数序列的类型。std::index_sequence_for则是一个模板别名,它根据给定的类型生成一个std::index_sequence。
在实际编程中,我们通常使用std::make_index_sequence来生成一个索引序列,然后使用这个序列来访问元组或数组的元素。这样,我们可以在编译时生成代码,而不需要在运行时进行循环。
std::make_index_sequence
的实现依赖于C++的模板元编程。它使用了递归模板和特化来生成一个包含递增整数的序列。
以下是一个简化的std::make_index_sequence
的实现:
template<std::size_t... Ints>
struct index_sequence {
};
template<std::size_t N, std::size_t... Ints>
struct make_index_sequence_helper : make_index_sequence_helper<N - 1, N - 1, Ints...> {
};
template<std::size_t... Ints>
struct make_index_sequence_helper<0, Ints...> {
using type = index_sequence<Ints...>;
};
template<std::size_t N>
using make_index_sequence = typename make_index_sequence_helper<N>::type;
在这个实现中,make_index_sequence_helper
是一个模板,它递归地生成一个包含递增整数的序列。当N为0时,递归结束,生成的序列被包装在index_sequence
中。
4.运用
4.1.打印序列的值
代码如下所示:
#include <array>
#include <iostream>
#include <tuple>
#include <utility>
template <typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq) {
std::cout << "The sequence of size " << int_seq.size() << ": ";
((std::cout << ints << ' '), ...);
std::cout << '\n';
}
// 转换数组为 tuple
template <typename Array, std::size_t... I>
auto a2t_impl(const Array &a, std::index_sequence<I...>) {
return std::make_tuple(a[I]...);
}
template <typename T, std::size_t N, typename Indices = std::make_index_sequence<N>>
auto a2t(const std::array<T, N> &a) {
return a2t_impl(a, Indices{});
}
// 漂亮地打印 tuple
template <class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple_impl(std::basic_ostream<Ch, Tr> &os, const Tuple &t, std::index_sequence<Is...>) {
((os << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template <class Ch, class Tr, class... Args>
auto &operator<<(std::basic_ostream<Ch, Tr> &os, const std::tuple<Args...> &t) {
os << "(";
print_tuple_impl(os, t, std::index_sequence_for<Args...>{});
return os << ")";
}
int main() {
print_sequence(std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
print_sequence(std::make_integer_sequence<int, 20>{});
print_sequence(std::make_index_sequence<10>{});
print_sequence(std::index_sequence_for<float, std::iostream, char>{});
std::array<int, 4> array = {1, 2, 3, 4};
// 转换 array 为 tuple
auto tuple = a2t(array);
static_assert(std::is_same<decltype(tuple), std::tuple<int, int, int, int>>::value, "");
// 打印到 cout
std::cout << tuple << '\n';
}
输出:
The sequence of size 7: 9 2 5 1 9 1 6
The sequence of size 20: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
The sequence of size 10: 0 1 2 3 4 5 6 7 8 9
The sequence of size 3: 0 1 2
(1, 2, 3, 4)
在上面的例子中,我们定义了一个 print_sequence
函数,它接受一个序列作为参数并打印其内容。我们通过std::make_index_sequence用不同的方法产生了多个序列并都能正确打印其值。
4.2.编译时求值
现在假如我们需编译期的一组1到4的平方值。你会怎么办呢?思考一下,可以这些写:
constexpr static size_t const_nums[] = {0, 1, 4, 9, 16};
int main() {
static_assert(const_nums[3] == 9);
}
这个代码肯定是正确的,但是如果4扩展到了20或者100?怎么办呢?我们可以用std::make_index_sequence和std::index_sequence来帮助我们实现这个逻辑:
template <size_t ...N>
static constexpr auto square_nums(size_t index, std::index_sequence<N...>) {
constexpr auto nums = std::array{N * N ...};
return nums[index];
}
template <size_t N>
constexpr static auto const_nums(size_t index) {
return square_nums(index, std::make_index_sequence<N>{});
}
int main() {
static_assert(const_nums<101>(100) == 100 * 100);
return 0;
}
首先我们定义了一个constexpr的静态函数const_nums。它通过我们本文的主角std::make_index_sequence来构造了一组0,1,2,3 .... N - 1的一组编译器的可变长度的整数列。(注意,这里调用std::make_index_sequence{}的构造函数没有任何意义,纯粹只是利用了它能够生成编译期整数列的能力。)
接着我们来看squere_num函数,这就是我们实际进行平方计算,并生成编译期静态数组的地方了,它的实现很简单,就是依次展开通过std::make_index_sequence生成的数字,并进行平方计算,最后塞到std::array的构造函数之中进行构造。
std::make_index_sequence
本身不执行任何操作;它只是生成一个整数序列。实际的遍历和操作通常在一个辅助函数中完成,该函数接受整数序列作为参数。
4.3.std::tuple访问值
之前在 C++之std::tuple(一) : 使用精讲(全) 中讲解了用 递归遍历元素和 std::apply 方式来访问std::tuple的元素值,这里我们来讲一下用std::index_sequence和std::make_index_sequence来访问std::tuple的元素值。示例代码如下:
template <typename Tuple, typename Func, size_t ... N>
void func_call_tuple(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 visitTuple(const std::tuple<Args...>& t, Func&& func) {
func_call_tuple(t, std::forward<Func>(func), std::make_index_sequence<sizeof...(Args)>{});
}
int main() {
auto t = std::make_tuple(155, false, 566.90, "hello world!");
visitTuple(t, [](auto&& item) {
std::cout << item << ",";
});
}
这个代码首先定义了一个travel_tuple的函数,并且利用了std::make_index_sequence将tuple类型的参数个数进行了展开,生成了0到N - 1的编译期数字。
接下来我们再利用func_call_tuple函数和展开的编译期数字,依次调用std::get<N>(tuple),并且通过lambda表达式依次的调用,完成了遍历tuple的逻辑。
在C++17中,std::apply也利用std::make_index_sequence实现了std::apply,详情请查看std::apply源码分析_{ return std::forward<_fn>(__f)(std::forward<_args-CSDN博客
5.总结
std::index_sequence和std::make_index_sequence结合使用这种方法是模板元编程中的一个常见技巧,它允许我们在编译时生成和执行复杂的操作,而不需要在运行时付出任何额外的性能开销。