开个新坑,一堆破模板玩意





目录



概述

模板元编程可以说是C++中最困难也是最强大的编程范式。模版元编程不同于普通的运行期程序,它执行完全是在编译期,并且它操纵的数据不能是运行时变量,只能是编译期常量(且不可修改)。因此,模版元编程需要很多技巧,在这里你会遇到非常多的枚举常量、继承、模板偏特化、可变模板参数、类型萃取等方法,如果要读懂STL库,那么模板元编程必须要掌握;不仅如此,C++中的许多黑科技都是依赖于模板元编程的,例如我们甚至可以写出编译期排序的算法

模板元编程:template meta programing

前置知识

模板元编程又两个部分组成:元数据和元函数。元数据是指编译期能在编译期处理的数据,即编译期常量;元函数指操纵元数据的函数,在编译期调用,它通常表现为一个模板类或一个模板函数

模板元编程十分特殊,它无法使用​​if-else​​,​​for​​,​​while​​这些运行期语句,在模板元编程中,我们时常会使用到这些语法:

  • ​enum​​,static-constexpr:用来定义编译期的整数常量
  • ​using​​,​​typedef​​:定义元数据
  • T,Ts...:用来声明元数据类型
  • ​template​​:定义元函数
  • ​::​​:用来解析类型作用域获取元数据

constexpr

type_traits-定义元数据

​<type_traits>​​是C++11提供的模板元基础库,它提供了模板元编程中需要的常用的基础元函数

std::integral_constant,定义编译期常量

举个例子,C++11中提供了​​std::integral_constant​​来定义编译期常量

template<typename T>
using one_constant = std::integral_constant<T, 1>;

template<typename T>
struct one_struct : std::integral_constant<T, 1> {};


因此我们可以使用​​one_constant<int>::value​​或​​one_struct<int>::value​​来获取编译期常量int 1

而在C++11之前,我们定义这个常量就需要用到​​enum​​或static-const

struct one_struct
{
enum { value = 1 };
};
struct one_struct
{
static const int value = 1;
};


然后通过​​one_struct::value​​来访问值,其中​​enum​​能隐式转换为​​int​

​std::integral_constant​​的实现也非常的简单

template <class _Ty, _Ty _Val>
struct integral_constant {
static constexpr _Ty value = _Val;

using value_type = _Ty;
using type = integral_constant;

constexpr operator value_type() const noexcept {
return value;
}

_NODISCARD constexpr value_type operator()() const noexcept {
return value;
}
};


可以看到,通过C++11的​​<type_traits>​​提供的一个简单的​​std::integral_constant​​就可以很方便的定义编译期常量,而无需再去使用​​enum​​和static-const。紧接着,库中又提供了编译期常量的​​bool​​类型

template <bool _Val>
using bool_constant = integral_constant<bool, _Val>;

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;


std::integer_sequence

std::is_integral_v,判断是否为整形

​std::is_integral_v​​,根据名称可以看出它可以用于判断​​T​​是否为integral类型,从​​_v​​可以得出它是一个元数据。​​std::is_integral_v​​在对T去除cv限定符后,在一堆​​integral​​类型中进行匹配比较,最后萃取出一个​​bool​​编译期常量

// STRUCT TEMPLATE is_integral
template <class _Ty>
inline constexpr bool is_integral_v = _Is_any_of_v<remove_cv_t<_Ty>, bool, char, signed char, unsigned char,
wchar_t,
#ifdef __cpp_char8_t
char8_t,
#endif // __cpp_char8_t
char16_t, char32_t, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long>;


那么仅仅有一个“值”是不够的,我们还需要一个类型,那就是​​std::is_integral​​,这里从“值”到“类型”的“逆”转换很巧妙

为什么说是“逆”转换呢,因为这个类的声明顺序是现有​​std::is_integral_v​​,后有​​std::is_integral​

template <class _Ty>
struct is_integral : bool_constant<is_integral_v<_Ty>> {};


我翻了很久的库文件,主要都是使用​​std::is_integral_v​​,对于​​std::is_integral​​的使用非常少,或许这就是可以不用,但是不能没有吧

// true 两者都派生自std::bool_constant<false>
std::cout << std::boolalpha << std::is_base_of_v<std::false_type, std::is_integral<std::string>> << std::endl;
// true 因为long long是整形类型
std::cout << std::boolalpha << std::is_integral_v<long long> << std::endl;


std::integer_sequence,定义编译期整数序列

为什么说是整形呢,请看源码。如果不是整形类型那么IDE和编译期会给出静态断言报错

// 一个类型加上一个非类型模板参数包
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;

static constexpr size_t size() noexcept {
return sizeof...(_Vals);
}
};


来通过一个简单的例子来了解它

template<typename T, T... Is>
void print_sequence(std::integer_sequence<T, Is...> sequence)
{
((std::cout << sequence.size() << "elements: ") << ... << Is);
}

int main()
{
// 6 elements: 4328910
print_sequence(std::integer_sequence<unsigned int, 4, 3, 2, 8, 9, 10>());
// 10 elements: 0123456789
print_sequence(std::make_integer_sequence<int, 10>());
// 10 elements: 0123456789
print_sequence(std::make_index_sequence<10>());
}


其他的相关源码为

// 构建一个std::integer_sequence
// ALIAS TEMPLATE make_integer_sequence
template <class _Ty, _Ty _Size>
using make_integer_sequence = __make_integer_seq<integer_sequence, _Ty, _Size>;

template <size_t... _Vals>
using index_sequence = integer_sequence<size_t, _Vals...>;

// 构建一个std::integer_sequence<std::size_t>
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_integer_sequence能创建一个0-_Size - 1的序列,需要注意的是,它并不是一个可调用的函数,而是一个type alias(模板别名)
  • std::index_sequencestd::size_t类型的std::integer_sequence
  • std::index_sequence_for将以参数包的个数作为序列的_Size,创建一个0-_Size - 1的序列
    // 5 elements: 01234 print_sequence(std::index_sequence_for<float, std::iostream, char, double, std::shared_ptr<std::string>>{}); // 参数包大小为5

利用编译期序列实现数组到元组的转换

这是一个cppreference上的例子

template<typename Array, std::size_t... Is>
auto array_to_tuple_impl(const Array& _array, std::index_sequence<Is...> _sequence)
{
return std::make_tuple(_array[Is]...);
}

template<typename T, std::size_t I, typename Sequence = std::make_index_sequence<I>>
auto array_to_tuple(const std::array<T, I> _array)
{
return array_to_tuple_impl(_array, Sequence{});
}

int main()
{
std::array<int, 3> a{12, 3, 2};

auto full_t = array_to_tuple(a);
auto part_t = array_to_tuple<int, 3, std::index_sequence<1, 2>>(a);

// 32
std::apply([](auto&&... obj) { ((std::cout << obj << " "), ...); }, full_t);
}


但是这个输出​​std::tuple​​的方法并不完美,它会在输出的末尾添加一个不必要的​​" "​​,所以在此之上进行一层编译期序列的封装,能够判断此时解析到元组的第几个元素

template<typename Tuple, std::size_t... Is>
std::ostream& print_tuple_impl(std::ostream& os, const Tuple& tuple, std::index_sequence<Is...> sequence)
{
return ((os << (Is == 0 ? "" : " ") << std::get<Is>(tuple)), ...);
}

template<typename... Ts>
std::ostream& operator<<(std::ostream& os, std::tuple<Ts...> tuple)
{
return print_tuple_impl(os, tuple, std::index_sequence_for<Ts...>{});
// return print_tuple_impl(os, tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
// perfect print
std::cout << full_t << std::endl; // 12 3 2


type_traits-判断元数据

std::is_array,判断是否数组类型

源码十分简单,根据特化类型来判断数组类型

// TYPE PREDICATES
template <class>
inline constexpr bool is_array_v = false; // determine whether type argument is an array

template <class _Ty, size_t _Nx>
inline constexpr bool is_array_v<_Ty[_Nx]> = true;

template <class _Ty>
inline constexpr bool is_array_v<_Ty[]> = true;

// STRUCT TEMPLATE is_array
template <class _Ty>
struct is_array : bool_constant<is_array_v<_Ty>> {};


测试代码

float arr[10]{};
// true
std::cout << std::boolalpha << std::is_array_v<decltype(arr)> << std::endl;
// C++20 arr是一个有边界的数组 故输出false
std::cout << std::boolalpha << std::is_unbounded_array_v<decltype(arr)> << std::endl;
// false
std::cout << std::boolalpha << std::is_array_v<std::array<int, 10>> << std::endl;


std::is_same,判断两种类型是否相同

template <class, class>
inline constexpr bool is_same_v = false; // determine whether arguments are the same type
template <class _Ty>
inline constexpr bool is_same_v<_Ty, _Ty> = true;

template <class _Ty1, class _Ty2>
struct is_same : bool_constant<is_same_v<_Ty1, _Ty2>> {};


测试代码

// 一般情况int都占4B true
std::cout << std::boolalpha << std::is_same_v<int, std::int32_t> << std::endl;
// true
std::cout << std::boolalpha << std::is_same_v<int, signed int> << std::endl;


更多类型判断

C++中的模板元编程_模板元编程

更多内容可以查看文档:​​cppreference-类型属性​

type_traits-类型修改

根据上文中的​​std::is_array​​,我们可以看到原理是对不同的类型参数做出不同的特化。现在我们还可以利用特化做点别的事情

std::remove_const,移除类型的const属性

// STRUCT TEMPLATE remove_const
template <class _Ty>
struct remove_const { // remove top-level const qualifier
using type = _Ty;
};

template <class _Ty>
struct remove_const<const _Ty> {
using type = _Ty;
};

template <class _Ty>
using remove_const_t = typename remove_const<_Ty>::type;


测试代码为

std::remove_const_t<const int> data = 100;
data = 10;


std::decay,朽化

对于最终的结果​​type​​,​​std::decay​​做了很多工作

  • 首先将引用类型去除

// STRUCT TEMPLATE decay
template <class _Ty>
struct decay { // determines decayed version of _Ty
using _Ty1 = remove_reference_t<_Ty>;
using _Ty2 = typename _Select<is_function_v<_Ty1>>::template _Apply<add_pointer<_Ty1>, remove_cv<_Ty1>>;
using type = typename _Select<is_array_v<_Ty1>>::template _Apply<add_pointer<remove_extent_t<_Ty1>>, _Ty2>::type;
};

template <class _Ty>
using decay_t = typename decay<_Ty>::type;