【模板元编程和反射】(一):type_list

本文述说了基于匹配的模板以及常见模板小工具type_list

本章的type_list实现代码在mirrowtype_list.hpp中。可自行参考。

基于匹配的模板

C++的模板是基于匹配的,也就是说在实例化的时候会尝试匹配所有存在的模板。直到所有模板都不匹配才会报错。

类型萃取

类型萃取就是使用这种规则的模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// (1)
template <typename T>
struct remove_one_pointer {
    using type = T;
};

// (2)
template <typename T>
struct remove_one_pointer<T*> {
    using type = T;
};

template <typename T>
using remove_pointer_t = typename remove_one_pointer<T>::type;

int main() {
    using type1 = remove_pointer_t<int>;		// type -> int
    using type2 = remove_pointer_t<int*>;	// type -> int
    using type3 = remove_pointer_t<int**>;	// type -> int*
    return 0;
}

“萃取”是化学中的术语,意思是将某种混合物中的东西分离出来。这里的remove_one_pointer会移除一层指针,如果类型不是指针则会保留原本类型。

这里的type1给入的类型是int,编译器会先去匹配特化模板*(2),发现不能将int作为其模板参数(其模板参数需要一个指针T*),于是去匹配(1)*这个非特化模板,得到类型type = int

type2给入的类型是int*,匹配*(2)的时候传入一个int*,这个int*会匹配到9行的struct remove_on_pointer<T*>中的T*,而(2)*中的模板参数(我指typename T这个T)是T,所以T*会是int*T变为int。这样就成功萃取出来了。

type3也是同理,9行的struct remove_on_pointer<T*>中的T*会匹配到int**,然后注意到T是去掉一层指针(如果写struct remove_on_pointer<T**>就是去掉两个),所以T会是int*而不是int

萃取有个固定写法:

  1. 确定你需要对哪个类进行萃取,比如我们需要对一个特定的类std::list

  2. 确定你要对他的哪个模板参数进行萃取。std::list的声明是:

    1
    2
    3
    4
    
    template<
        class T,
        class Allocator = std::allocator<T>
    > class list;
    

    有两个模板参数TAllocator,比如我们要得到T

  3. 将需要萃取的类型的所有模板参数写在template <>声明中,并在特化部分将此类型用上,然后在结构体内用using type = XXX得到你想要的类型:

    1
    2
    3
    4
    5
    6
    7
    8
    
    template <typename T> // 要萃取T,我们这里写一个T
    struct get_list_element_type;	// 先写一个声明,声明不做任何事情
    
    template <typename T, typename Allocator>
    struct get_list_element_type<std::list<T, Allocator>> {
      	using type = T;
        // using allocator = Allocator;		// 想要保存Allocator的类型?也可以
    };
    
  4. 使用时,直接将类型放入你写的萃取中就行了:

    1
    2
    3
    4
    5
    6
    7
    
    using type = typename get_list_element_type<std::list<int>>::type;	// int
    
    // 一般会给一个更加方便的using来节省打字时间:
    template <typename T>
    using get_list_element_type_t = typename get_list_element_type<T>::type;
    
    using type = get_list_element_type_t<std::list<int>>;
    

当然这里只是个例子,对于std::list来说,标准库很贴心地在其内部早就存下了元素类型std::list<T>::value_type和内存分配器类型std::list<T>::allocator_type

将模板看做编译期的函数

上面的get_list_element_type我们可以看做是对类型操作的函数:

1
2
3
using type = get_list_element_type_t<your-list>;
// 你不觉得很像:
auto  type = get_list_element_type_t(your-list);

模板参数作为入参,模板内的type或者value作为返回值。

不只是萃取,你可以写其他的模板来对类型/编译期值进行操作:

1
2
3
4
5
6
7
template <size_t Idx>
struct inc_idx {
    static constexpr size_t value = Idx + 1;
};

template <size_t idx>
constexpr size_t inc_idx_v = inc_idx<Idx>::value;

这里inc_idx会将value+1。

标准库也有一些类似的做法,比如std::conditional_t可以做编译时if

type_list小工具

在很多反射和模板元编程框架中(Refl-CppUbp.a USRefl)都会有type_list小工具。这个工具顾名思义,是编译时存储和操作一组类型的列表。

模板元编程在很大程度上和函数式编程有着极为密切的关系。而我们这里的type_list实现也是仿造函数式编程,尤其是Haskell中list的实现。

type_list的用法

首先来看一下这个工具的用途(一部分):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using list = type_list<int, char, float, double>;	// 声明一个类型列表

// list_element_t 用于从 type_list中获得第n个元素
static_assert(std::is_same_v<list_element_t<list, 0>, int>);
static_assert(std::is_same_v<list_element_t<list, 1>, char>);
static_assert(std::is_same_v<list_element_t<list, 2>, float>);
static_assert(std::is_same_v<list_element_t<list, 3>, double>);

// list_head_t 是仿照函数式编程中 head 函数,用于获得list开头的元素
static_assert(std::is_same_v<list_head_t<list>, int>);

// list_size_v 用于获得list的大小
static_assert(list_size_v<list> == 4);

// 一个谓词(谓词:函数式编程术语,即返回布尔值的函数),用于判断某个类型T是否是`int`,使用基于匹配的方法编写
template <typename T>
struct IsInt {
    static constexpr bool value = false;
};

template <>
struct IsInt<int> {
    static constexpr bool value = true;
};

// disjunction_v,仿照std::disjunction,用于判断列表中是否至少有一个元素使得谓词返回true
static_assert(disjunction_v<list, IsInt>);
// conjunction_v,仿照std::conjunction,用于判断列表中是否所有元素使得谓词返回true
static_assert(!conjunction_v<list, IsInt>);

// list_filter_t,使用谓词过滤列表,所有满足谓词的元素会被保留下来
static_assert(std::is_same_v<list_filter_t<list, std::is_integral>, type_list<int, char>>);
static_assert(std::is_same_v<list_filter_t<type_list<>, std::is_integral>, type_list<>>);

type_list的实现

type_list声明

首先是type_list的声明:

1
2
template <typename... Ts>
struct type_list {};

是一个空类,所有的类型信息全部记录在不定模板参数Ts中。

list_size_v的实现

接下来实现第一个函数,也是最简单的,list_size_v,用于获得type_list中元素个数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
namespace detail {

// (1)
template <typename>
struct list_size;

// (2)
template <template <typename...> typename ListType, typename... Ts>
struct list_size<ListType<Ts...>> {
    static constexpr size_t value = sizeof...(Ts);
};

}	// namespace detail

template <typename T>
static constexpr size_t list_size_v = detail::list_size<T>::value;

模板元编程中,习惯性的做法是将实现放在命名空间detail或者impl中,用户一般是不允许接触这种命名空间的(当然编译器并没有强制禁止,只是口头约定),然后在外部暴露一个便捷方法(这里是list_size_v)。

这里的实现也很简单,依旧是基于匹配的模板方法:将要匹配到的模板类型中所有需要的类型参数(这里是type_list需要的参数,是*(2)*中的Ts)写在template <>中,然后在特化处struct list_size<>写上你需要的真正类型。

这里其实可以写成:

1
2
3
4
5
// (2)
template <typename... Ts>
struct list_size<type_list<Ts...>> {
    static constexpr size_t value = sizeof...(Ts);
};

告诉list_size我们需要的参数是固定类型type_list。这里之所以写了模板模板参数template <typename...> typename ListType是因为这样写可以匹配到std::tuple(因为tuple的模板声明也是template <typename... Ts> class tuple{ ... };,通用性更好一些。如果你不需要就不用这样写。

内部实现的话使用sizeof...对不定模板参数计数就行了。

如果传入的类型T不可以接受不定模板参数Ts,那会匹配到*(1)*处的声明,编译器会发现类没有实现,所以会报一个类缺少实现的编译时错误。

list_head_t的实现

接下来实现一个复杂一点的。list_head_t通过给入一个type_list可以得到此list的第一个元素。如果type_list是空则编译无法通过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// (1)
template <typename>
struct list_head;

// (2)
template <template <typename...> typename ListType, typename T, typename... Remains>
struct list_head<ListType<T, Remains...>> {
    using type = T;
};

template <typename T>
using list_head_t = typename list_head<T>::type;

*(1)处仍然是声明。(2)*处将type_list内元素拆分成两部分:第一个元素T以及剩下的所有元素Remains,然后通过匹配拿到第一个元素。

list_add_to_first的实现

list_add_to_first<List, T>会将元素T插入List的第一个位置上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename List, typename T>
struct list_add_to_first;

template <template <typename...> typename ListType, typename... Ts, typename T>
struct list_add_to_first<ListType<Ts...>, T> {
    using type = ListType<T, Ts...>;
};

template <typename List, typename T>
using list_add_to_first_t = typename list_add_to_first<List, T>::type;

具体实现中通过将新元素T和老元素们Ts放在一个ListType中并返回。

list_element_t的实现

看了上面三个例子后,对如何使用模板参数以及如何匹配想必已经有一定的了解了。接下来看一点不一样的。

list_element_t<ListType, Idx>可以取得ListType中第Idx个元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename T, size_t Idx>	// 这里的模板参数没用到可以省略,但我还是写出来以便于下面解说
struct list_element;

// (1)
template <template <typename...> typename ListType, typename T, typename... Ts,
          size_t N>
struct list_element<ListType<T, Ts...>, N>
    : list_element<ListType<Ts...>, N - 1> {};

// (2)
template <template <typename...> typename ListType, typename T, typename... Ts>
struct list_element<ListType<T, Ts...>, 0> {
    using type = T;
};

这里用了递归式模板:*(2)*中定义递归结束条件:当Idx==0的时候,直接返回ListType的第一个元素。

而*(1)*中则进行递归:我们构造传入ListType的子列表作为下一次递归的开始(这个列表只是将第一个元素移除了),并将Idx - 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
假设有type_list: tl = int char float double
我们要拿到下标为2的元素,也就是float Idx = 2
    
调用list_element_t:
	list_element<type_list<int, char, float, double>, 2>
第一次递归的调用:
    list_element<type_list<char, float, double>, 1>
第二次递归的调用:
    list_element<type_list<float, double> 0>
这时Idx == 0,匹配到(2)处的特化模板,现在list_element结构体里有T = float了

这里*(1)处的继承并没有任何面向对象里as-is*的意思,单纯地就是将数据聚拢在一起。一般在模板元编程中,类如果都是空的话,比较趋向于使用集成将信息组合到一起。

list_foreach_t的实现

接下来我们要更加贯彻将模板视为编译期函数的原则。

list_foreach_t<List, Pred>通过给入一个Pred模板类,将这个类当做函数用在List的所有元素上从而创造一个新的List

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename List, template <typename> typename F>
struct list_foreach { };

// (2)
template <template <typename...> typename ListType, template <typename> typename F, typename... Ts>
struct list_foreach<ListType<Ts...>, F> {
    using type = ListType<typename F<Ts>::type ...>;
};

template <typename List, template <typename> typename F>
using list_foreach_t = typename detail::list_foreach<List, F>::type;

*(2)*中的函数类型是template <typename> typename F,表示接受一个模板参数,并且在7行的typename F<Ts>::type也要求其内部有一个type类型作为返回值。比如:

1
2
3
4
template <typename T>
struct AddPointer {
    using type = T*;
};

就是合法的,可以这样用:

1
list_foreach_t<type_list<int, char>, AddPointer>

其他

对于type_list还有很多函数可以编写,比如筛出其中某个元素,将type_list倒置等。但最基本的写法和例子都写在上面了。完整代码可去github上看一下。

updatedupdated2023-10-272023-10-27