开个新坑,一堆破模板玩意
目录
模板元编程可以说是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_sequence
是std::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
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;
更多类型判断
更多内容可以查看文档: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;