zoukankan      html  css  js  c++  java
  • 初识C++模板元编程(Template Mega Programming)

    前言:毕设时在开源库上做的程序,但是源码看得很晕(当时导师告诉我这是模板元编程,可以不用太在乎),最近自己造轮子时想学习STL的源码,但也是一样的感觉,大致了解他这么做要干什么,但是不知道里面的机制。于是开始学习《C++模板元编程》,看完第二章对一些东西豁然开朗。

    PS:该书也有点老了,C++11标准还没出来,主要用的Boost库。

    Traits(特征)

    说正题,在STL中经常可以见到后缀为traits的名字,比如经常用到的std::string,本质是类模板basic_string的第一个参数charT为char的实例化。相应的,用来表示宽字节的字符串模板类wstring,charT为wchar_t。

    template < class charT,
               class traits = char_traits<charT>,    // basic_string::traits_type
               class Alloc = allocator<charT>        // basic_string::allocator_type
               > class basic_string;
    typedef basic_string<char, char_traits<char>, allocator<char> >
     string;
    

     根据模板声明,3个模板参数,charT表示存放字符类型(Char Type);Alloc则是allocator<T>的实例化,通常的new是一次性分配完内存再在内存上构造T类型的元素,allocator<T>则分离了这两个过程,可以先分配一段连续的内存空间,相当于内存池,再直接在上面构造T类型元素,不需要使用时销毁元素即可,内存空间并未释放,可以重新在上面构造新的元素。这样就省去了每次都重新分配空间再构造元素的过程。有点类似对C的malloc/free的封装。(没仔细研究,不去祥述)

    第2模板参数traits则是char_traits模板参数为charT的实例化

    template<class _Elem>
        struct char_traits
            : public _Char_traits<_Elem, long>
        {    // properties of a string or stream unknown element
        };
    

    可以发现char_traits继承于由_Char_traits的实例化的模板类,可以提供扩展。这里没有扩展,基本可以将char_traits等价模板类于_Char_traits<_Elem, long>

    template<class _Elem,
        class _Int_type>
        struct _Char_traits
        {    // properties of a string or stream element
        typedef _Elem char_type;
        typedef _Int_type int_type;
        typedef streampos pos_type;
        typedef streamoff off_type;
        typedef _Mbstatet state_type;
        // ...
        }
    

    省略了后面的静态方法,可以看出在此之前有5个typedef。在C++中,类的内部可以用typedef定义类型别名,可以像使用静态成员一样使用它。
    也就是说可以用模板类_Char_traits<_Elem, _Int_type>::char_type来得到类型名。结合之前所说,char_traits继承自该模板类,因此可以用char_traits::char_type来得到类型名。可以发现,对模板类进行继承有点像对模板类进行实例化获取类型别名(比如typedef std::map<int, std::string> MapIntStr;),大大减轻了键盘输入的压力。

    回过头来,std::basic_string的第2个模板参数traits是由字符类型charT作为模板参数(_Elem)实例化的char_traits

    也就是说在std::basic_string中,通过模板参数traits可以直接获得类型。  比如traits::char_type等价于charT。

    问题来了:为什么要特意给模板参数再套到一个模板上?

    直接借用书上对迭代器traits的例子了,跟char_traits其实是类似的。

    当我们需要对迭代器进行交换时(功能是交换迭代器指向的值)

        template <class FwIt1, class FwIt2>
        void iter_swap(FwIt1 i1, FwIt2 i2)
        {
            typename FwIt1::value_type tmp = *i1;
            *i1 = *i2;
            *i2 = tmp;
        }
    

     这个实现有个问题,FwIt必须就像char_traits一样,内部有句typedef把某种类型取个别名叫value_type。如果是自己实现新的迭代器,可以一直按照这个标准在每个类模板里加上对应的typedef。

    问题在于,且不谈其他的类模板是否实现了typedef,就指针而言在iter_swap中是不兼容的。因此需要加个像适配器一样的东西,让指针也能接入到iter_swap中。

    现在我们编写一个iterator_traits作为适配器。

        template <class Iter>
        struct iterator_traits
        {
            typedef typename Iter::value_type value_type;
        };
    

    类中只有一行代码进行类型别名定义,这样iterator_traits<Iter>::value_type就等价于Iter::value_type,用类型实例化某个类模板来取代类型自己。

    C++模板有个优秀的特性是特例化(specialization),当模板参数有着某种特别类型时,选择实例化“特例类型”而不是通用模板。比如std::vector<bool>和其他一般的std::vector<T>的内部实现是不一样的。

    对指针类型进行特例化后

        template <class T>
        struct iterator_traits<T*>
        {
            typedef typename T value_type;
        };
    

    这样假如是用指针类型来实例化iterator_traits,将会选择这个来实例化,而不是前一个通用形式。(选择哪一个模板实例化这里也不详述,《C++ Primer》有相关说明)
    原来的iter_swap就变成了

        template <class FwIt1, class FwIt2>
        void iter_swap(FwIt1 i1, FwIt2 i2)
        {
        //    typename FwIt1::value_type tmp = *i1;
            typename
                iterator_traits<FwIt1>::value_type 
            tmp = *i1;
            *i1 = *i2;
            *i2 = tmp;
        }
    

    可以看出,在模板参数之上再套个模板是起到适配器的作用,在类模板中需要用更通用的方式取得类型信息,相当于在一个体系内达成统一的标准。

    C++标准库已经实现了iterator_traits,只有迭代器才能用作模板参数,因为只有迭代器类实现了那几个特定的typedef。另外,iterator_traits也针对指针类型进行了特例化。假如用容器作为模板参数,会报错(比如没有实现哪个typedef)。

    回到最初的问题:什么是模板元编程?使用这种traits技巧跟模板元编程有什么关系?

    简要地解释元编程(Meta Programming),元编程类似一个解释器,比如C/C++都是生成了汇编最终生成机器码让计算机运行的。

    而模板元编程(TMP)是在C++的体系内,模板实例化后会变成新的代码,但这份新的代码仍然是C++代码,模板的优点就像它的名字一样,套个不同的参数能够刻画出一组组功能相似类型又不同的类或函数。比如std::vector<int>和std::vector<float>就是两种类型,但是却有着通用的方法。

    模板的实例化是发生在编译期间的,这是C++ TMP的根本原因,能够在编译期间完成的事情不需要在运行期间做到,虽然编译时间更长,但是会得到运行效率的提升。

    直接给出《C++模板元编程》第一章的例子,使用模板元编程来把二进制数转换成十进制数,在编译好时已经计算出了实例化的值。

    template <unsigned long N>
    struct binary
    {
        static unsigned const value
            = binary<N / 10>::value << 1
            | N % 10;
    };
    
    // 特化
    template <>
    struct binary<0>
    {
        static unsigned const value = 0;
    };
    

    它的思想是,一段短的类模板代码等价于好长好长的一个常量表达式(编译期间就确定),在运行时使用binary<110010>::value相当于直接使用一个常量表达式(用作赋值、实参等等)。而若按照传统的方法写个函数来计算,就是在运行时进行计算。

    而类型跟值本质上是一样的,就像我们在类中进行了typedef,在类外部使用别名的方式就跟使用静态成员方式一样。模板元编程不仅仅是做这种数值运算,也可以做类型运算,前面花了较大大篇幅提到traits就是一个典例。

  • 相关阅读:
    A1052. Linked List Sorting (25)
    A1032. Sharing (25)
    A1022. Digital Library (30)
    A1071. Speech Patterns (25)
    A1054. The Dominant Color (20)
    A1060. Are They Equal (25)
    A1063. Set Similarity (25)
    电子码表
    矩阵键盘
    对象追踪、临时对象追踪、绝对坐标与相对坐标
  • 原文地址:https://www.cnblogs.com/Harley-Quinn/p/5596809.html
Copyright © 2011-2022 走看看