前言
本文只是针对TMP(Template Metaprogramming)的简单说明,并不涉及模板元编程中的各种黑魔法诸如编译期堆排序,实现反射特性等等。
旨在对TMP做一个入门科普,以及简单介绍TMP在实际业务中存在哪些应用。
前置知识
C++模板特化,偏特化的一些匹配原则。(参考C++ Primer相关章节即可)
C++模板类型推导的基本原理,以及模板类型推导的一些常见问题。
具体可见Effective Modern C++ Item1。
模板元编程的基本概念
顾名思义,模板元编程就是用模板来实现元编程,它工作于编译期,而非运行期。
开发者尝试使用模板实例化的一些特性和约束来完成一些计算任务,或者实现一些在运行期很难完成的功能。
TMP的简单应用(编译期求值)
abs
abs相当于是TMP内的”hello,world”, 我们通过它来了解最基础的模板元编程。
1 | template <int N> // use template parameter as a function parameter |
fibonacci
为了展现tmp并非只能完成简单计算求值,有fibonacci的TMP递归实现如下:
1 | template<unsigned int N> |
不过编译期递归并不能为所欲为,其递归深度取决于编译器允许的模板实例化的最大深度,不过可以通过-ftemplate-depth=N
进行调整。
(constexpr函数同样存在编译期递归深度限制问题)
TMP的简单应用(操作类型)
到目前为止,给出的2个示例并不能真正体现TMP的优势,毕竟我们完全可以用constexpr函数取代abs<>和fibonacci<>。 TMP的优势在于,它的参数可以是一个类型,而非一个值。实际上,c++内早就有这种函数,例如sizeof。
1 | int main() { |
输入类型,输出值
假设存在一个场景,我们需要根据指定类型返回一个值,例如根据指定类型,返回该类型的最大值或者最小值(也就是std::numeric_limits
)。 那么可以有简单实现如下:
1 | template<typename T> |
可以注意到,对于已经完成特化的部分类型,max_value运行地很好,但对于其他类型,将会直接导致编译报错。
这种技法被称之为SFINAE(Substitution Failure Is Not An Error)。SFINAE是元编程基础组件std::enable_if
的重要基础,后续会继续提到。
输入类型,输出类型
也许我们不应该仅仅将目光停留在输出数值上,更进一步地,TMP可以输出一个类型,而不仅仅是数值。
type traits
模板函数可以根据输入类型的一些特性,返回特定类型。在某些场景下这种名叫type traits的技法非常有用,例如stl中的大部分泛型算法都基于这种技法。
问题实例
举一个最简单的例子,让某个迭代器移动指定的步长, advance
函数声明大致类似于1
2
3
4template<class InputIterator, class Distance>
inline void advance(InputIterator &i, Distance n) {
... // impl
}
问题分析
众所周知,stl内各容器的迭代器均为具体的类型,并且具备不同的特性,以vector举例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template<class T, class Alloc = simpleAlloc<T>>
class vector {
public:// alias declarartions
using value_type = T;
using pointer = value_type *;
using iterator = value_type *;// iterator is raw pointer
using const_iterator = const value_type *;
using const_reference = const value_type &;
using size_type = size_t;
using difference_type = ptrdiff_t;
private:// data member
// iterator to indicate the vector's memory location
iterator start;
iterator finish;
iterator end_of_storage;
}
对于vector而言,它的迭代器类型为value_type *
,单纯的指针类型,支持随机访问。
而list
则不然,
1 | template<class T> |
list的迭代器显然仅仅支持双向递增递减,并不支持随机访问。
slist(单链表)的迭代器具体实现此处略过不再赘述,不过它的迭代器仅支持前向递增。
显然,枚举出所有迭代器并一一重载是不现实的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template<typename T>
inline void advance(slist<T>::iterator &i, size_t n) {
while (n--) ++i;
}
template<typename T>
inline void advance(list<T>::iterator &i, int n) {
if (n >= 0)
while (n--) ++i;
else
while (n++) --i;
}
template<typename T>
inline void advance(vector<T>::iterator &i, int n) {
i += n;
}
解决方案
stl将迭代器分为以下5种类型,并存在以下继承关系:
1 | struct input_iterator_tag {}; |
并且定义迭代器type traits为:
1 | template<class Iterator> |
因此,自定义迭代器只需要在内部声明iterator_category
和difference_type
(为了避免忘记声明,stl提供了一个模板以供继承),即可使用type_traits。 因此,advance
函数具体实现为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24template<class InputIterator, class Distance>
inline void _advance(InputIterator &i, Distance n, input_iterator_tag) {
while (n--) ++i;
}
template<class InputIterator, class Distance>
inline void _advance(InputIterator &i, Distance n,
bidirectional_iterator_tag) {
if (n >= 0)
while (n--) ++i;
else
while (n++) --i;
}
template<class InputIterator, class Distance>
inline void _advance(InputIterator &i, Distance n,
random_access_iterator_tag) {
i += n;
}
template<class InputIterator, class Distance>
inline void advance(InputIterator &i, Distance n) {
_advance(i, n, iterator_category_t<InputIterator>());
}
拓展
事实上,type traits在stl泛型算法中的应用随处可见(这也与stl的设计理念有关),例如:
1 | template<class T> |
不胜枚举。
构建模板元编程的基础组件
模板元编程中的代码复用
问题实例
假设存在一个场景,开发者需要对输入的类型添加或删除某种属性。
先从最简单的例子开始———擦除某种类型的const
属性:1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
struct remove_const {
using type = T;
};
template<typename T>
struct remove_const<const T> {
using type = T;
};
template<typename T>
using remove_const_t = typename remove_const<T>::type;
移除volatile
也是类似的,如:1
2
3
4
5
6
7
8
9template<typename T>
struct remove_volatile {
using type = T;
};
template<typename T>
struct remove_volatile<volatile T> {
using type = T;
};
擦除reference
也是类似的,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<typename T>
struct remove_reference {
using type = T;
};
template<typename T>
struct remove_reference<T&> {
using type = T;
};
template<typename T>
struct remove_reference<T&&> {
using type = T;
};
显然,add_lreference
,add_rrefernce
的实现也并不复杂,此处不再赘述。
问题分析
前文所提及的所有模板元案例,要么基于xxx::value
,要么基于xxx::type
(表示TMP的返回是一个类型还是一个值)。
每一次模板元编程都写一次using type = xxx
是一次非常磨人的行为,类似的代码理论上应该被复用。因此,我们有必要实现一些基础组件,并根据这些基础组件来完成模板元编程的开发工作。
TMP基础组件
证同的实现与应用
证同操作是一个单纯的投射,输入一个T类型,返回一个T类型。1
2
3
4template<typename T>
struct identity {
using type = T;
};
通过identity
,remove_xxx
的实现可以更加优雅:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<typename T>
struct remove_const: identity<T> {};
template<typename T>
struct remove_const<const T>: identity<T> {};
template<typename T>
struct remove_reference : identity<T> {};
template<typename T>
struct remove_reference<T&>: identity<T> {};
template<typename T>
struct remove_reference<T&&>: identity<T> {};
编译期数值的实现与应用
1 | template<typename T, T val> |
根据integral_constant
,开发者可以使用integral_constant<T, val>
来创建和使用编译期数值,例如:1
2
3
4
5template<bool val>
using bool_constant = integral_constant<bool, val>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
how to impl is_same?
is_same
,顾名思义,返回两个类型是否相同,其具体实现如下:1
2
3
4
5template<typename T, typename>
struct is_same : false_type {};
template<typename T>
struct is_same<T, T> : true_type {};
how to impl is_void ?
操作类型的基础是了解当前传入的类型,在泛型编程中,我们时常需要类似于is_int
,is_float
,is_pointer
等等诸如此类的类型判断。
本文并不关注描述如何实现以上工具,只是试图表明以上工具均可以通过前文已有所涉及的基础组件加以实现。
还是从最简单的例子开始,尝试通过几种方法来实现 is_void
。
通过特化穷举所有可能
1 | template<typename T> |
use is_same && remove_cv
1 | template<typename T> |
use is_one_of
is_one_of
,顾名思义,返回一个类型是否存在于类型列表中(not in std),其具体实现如下1
2
3
4
5
6
7
8template<typename T, typename ...Args>
struct is_one_of; // only a declaration
template<typename T>
struct is_one_of<T> : false_type {}; // 递归基
template<typename T, typename U, typename ...Args>
struct is_one_of<T, U, Args...> : bool_constant<is_same<T, U>::value || is_one_of<T, Args...>::value> {};
如何使用is_one_of
来实现is_void
是非常显然的:1
2template<typename T>
struct is_void: is_one_of<T,void, const void, volatile void, const volatile void> {};
以上关于is_void
的实现充分说明,类似于普通函数,模板元编程在实现过程中同样存在多种方案可供选择,并非仅有单一实践。
编译期判断
conditional
编译期同样存在if判断的需求与使用场景,例如某个模板类需要根据泛型参数T
的具体属性来决定继承基类B1
或B2
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class B1 {
...// sth
};
class B2 {
...// sth
};
template<bool val, typename T, typename U>
struct conditional {
using type = xxx;
};
template<typename T>
class D : public conditional<std::is_void<T>::value, B1, B2>::type {
};
其中conditional
是一个标准的元编程实现,其作用为:val为true,则type == T,否则type == U。
显然,conditional
的实现并无特别:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<bool val, typename T, typename>
struct conditional {
using type = T;
};
template<typename T, typename U>
struct conditional<true, T, U> {
using type = T;
};
template<typename T, typename U>
struct conditional<false, T, U> {
using type = U;
};
enable_if
enable_if
的实现基础来自于SFINAE,在TMP中使用非常广泛。
在实际开发工作中经常存在某种场景,即当前泛型函数仅应该支持符合条件的某些类型,其余类型应当在编译期触发报错,例如:1
2
3
4
5
6
7
8
9
10// 该模板函数接受参数:整形,浮点,string_view, xxx库提供的数组与字典结构
template <typename T,
typename std::enable_if<
std::is_same<T, std::string>::value ||
std::is_same<T, xxx::string_view>::value || std::is_integral<T>::value ||
std::is_floating_point<T>::value || std::is_same<T, xxx::Values>::value ||
std::is_same<T, xxx::Dictionary>::value>::type * = nullptr>
void process(const std::string &s, const std::string &t, const T &val) {
... // do sth
}enable_if
有实现如下:1
2
3
4
5
6
7template<bool val>
struct enable_if {}; // nothing in struct
template<>
struct enable_if<true> {
using type = void; // dont need use this type
};
模板元编程的实际应用
判断类型是否存在某个对外暴露的符号
简单介绍一下TMP在实际工程中的应用情况,例如:判断当前类型是否内含有某public member function或者pulic data member,思路和enable_if
思路大致类似。
这里仅以如何判断当前类型存在拷贝赋值函数举例。
先验知识
decltype 与 declval
https://xander.wiki/2018/06/25/%E4%BA%86%E8%A7%A3decltype%E6%8A%80%E6%9C%AF/
https://stdrc.cc/post/2020/09/12/std-declval/
void_t
https://blog.csdn.net/ding_yingzi/article/details/79983042
具体实现
1 | template<typename T> |
显然,如需检验某个类是否具备public data member,只需要更改decltype
内的判断条件即可。