STL

STL解析 —— 模板在traits技法中的应用

Standard Template Library —— template in traits

ZingLix April 21, 2018

介绍

在STL的实现中,traits技法是最核心的技术之一,其实现大量的运用到了模板这一特性,在Modern C++中引入的变长模板参数等等都为其实现创造了方便。

以C++11标准中std::pointer_traits为例,如下是标准中非特化版本中成员类型和成员别名模板的定义。

类型 定义
pointer Ptr
element_type 若存在则为 Ptr::element_type 。否则若 Ptr 是模板实例化 Template<T, Args...> 则为 T
differencenc_type 若存在则为 Ptr::difference_type ,否则为 std::ptrdiff_t
template <class U> using rebind 若存在则为 Ptr::rebind<U> ,否则若 Ptr 是模板实例化 Template<T, Args...> 则为 Template<U, Args...>

可以看到要求判断是否存在某一成员,然而其作为定义并不能用if语句实现,同时C++中也没有提供判断是否存在相关类型的函数,涉及到类型信息传递只能使用模板,特化某个情况下来达到目的。

声明

下面是pointer_traits非特化版本的声明,去除了与本文无关的内容,涉及到判断的转交由另一个模板来生成。

1
2
3
4
5
6
7
8
9
10
11
template<class Ptr>
struct pointer_traits
{
    using element_type = typename _element_type<Ptr>::type;
    using pointer = Ptr;
    using difference_type = typename _ptr_difference_type<Ptr>::type;

    template<class _Other>
    using rebind = typename _rebind_alias<Ptr, _Other>::type;
    // Other....
};

另外在这引入一个用于模板特化的声明,虽然它只是给void起了个别名,但是他前面有模板使其带有了类型信息。这可以看成一个trick,我们会在之后看到如何运用这个。

1
2
template<class... _Types>
using void_t = void;

element_type

这个模板用来得到元素类型element_type,我们先来回忆下它的要求:若存在则为 Ptr::element_type 。否则若 Ptr 是模板实例化 Template<T, Args...> 则为 T。然后根据下面的实现来理解整个过程。

1
2
3
4
5
6
7
8
9
10
11
template<class T, class = void>
struct _element_type
{
    using type = typename _first_parameter<T>::type;
};

template<class T>
struct _element_type<T, void_t<typename T::element_type>>
{
    using type = typename T::element_type;
};

先假设我们传入的类型具有T::element_type,那么就会产生对应的void_t<typename T::element_type>,从而满足特化条件调用第二个生成模板,那么就可以使typeT::element_type。相反,如果不存在T::element_type,那么就会调用第一个非特化版本实例化模板。第一个版本会利用另一个模板提取出类型,将在下面讨论。

从上面的例子可以看到,void_t虽然本身无意义,但是我们使其带有了类型信息,那么编译器就会检查是否满足特化条件,从而可以用来判断是否存在某一元素类型,以达到生成不同版本的目的。

现在我们再回头来看之前提到的_first_parameter<T>

1
2
3
4
5
6
7
8
9
template<class T>
struct _first_parameter;

template<template<class, class...> class T,
    class First, class... Other>
struct _first_parameter<T<First, Other...>>
{
    using type = First;
};

第一个声明并没有给出实现因为我们只要特定情况下的版本,那么如果实例化了直接报错即可,我们将重点放到第二个版本。第一个类型T是另一个模板<class,class...>,以满足要求中判断Ptr是否为Template<T, Args...>,第二个类型FirstT模板中第一个类型,最后可变长模板以适应不同类型数量的情况。在T类型中并没有显示给出类型名,因为我们在特化条件中指明了FirstOther...作为其类型,其他的交由编译器自行推导。

ptr_difference_type

这个相比前者实现较为简单,但思想相同,依旧根据要求来实现:若存在则为 Ptr::difference_type ,否则为 std::ptrdiff_t

1
2
3
4
5
6
7
8
9
10
11
template<class T, class = void>
struct _ptr_difference_type
{
    using type = ptrdiff_t;
};

template<class T>
struct _ptr_difference_type<T, void_t<typename T::difference_type>>
{
    using type = typename T::difference_type;
};

依旧是利用void_t来判断是否具有T::difference_type这一元素类型,从而调用特化和非特化两个版本使得type为不同的类型。

rebind

这个别名模板用于重新绑定类型名,所以根据要求,如果已有Ptr::rebind<U>那么直接调用这个即可,如果没有就要将Template<T, Args...>T替换得到 Template<U, Args...>

1
2
3
4
5
6
7
8
9
10
11
template<class T, class Other, class = void>
struct _rebind_alias
{
    using type = typename _replace_first_parameter<Other, T>::type;
};

template<class T, class Other>
struct _rebind_alias<T, Other, void_t<typename T::template rebind<Other>>>
{
    using type = typename T::template rebind<Other>;
};

同样用和之前一样的技巧利用void_t获得类型信息来对应不同的特化版本。我们再来仔细看第一个版本中_replace_first_parameter

这个目的很简单,就是将模板中第一个类型替换。

1
2
3
4
5
6
7
8
9
template<class NewFirst, class T>
struct _replace_first_parameter;

template<class NewFirst, template<class, class...>class T,
    class OldFirst, class ... Other>
struct _replace_first_parameter<NewFirst, T<OldFirst, Other...>>
{
    using type = T<NewFirst, Other...>;
};

第一个版本不应被实例化,所以没有实现。第二个版本第一个参数为新的类型,之后的与之前获取_first_element技巧相同,这样我们就能直接使用类型名,达到目的。

后记

模板在C++中有关类型信息传递有着不可或缺的作用,在STL的实现中也大量被使用。在实现过程中,其实我们只是给出了相应的特化条件,具体的类型推导和调用不同版本都由编译器在编译期自行完成,没有运行时的额外开销。