模板相关知识总结

发布时间:2014-5-30 16:20
分类名称:Boost


注意
这篇文章很多是由自己总结,甚至有的可能是凭空想象,可能有很多错误和不严谨的地方,读者需要带着怀疑和批判的眼光来读本文。

定义一个模板

template< parameter1, parameter2, …>
functions / class definition

Parameter 一般有三种类型:

  1. type parameter(类型参数) : typename T. T可以是系统固有的类型如int,long,也可以是用户自定义类型。
  2. Nontype parameter (非类型参数): const value。表示方法如int n,char const *szStr,诸如此类的表示。有点像一个函数参数,但是又有限制,模板实例化时,只能传入const value。为何不能为变量?因为变量是运行时确定的,而模板是在编译时确定的。Const类型可以在编译时确定下来。
  3. template type parameter(模板类型的参数):将一个类模板作为模板参数。

所以模板的定义可以细化如下:

template<[type parameters, …], [Nontype parameters, …], [template type parameters,…] …>
functions / class definition

[] 代表Option,可选。

下面举例说明:

只有type parameters的情况
template <class Type>
class QueueItem;

template <typename Type>
inline Type min( Array<Type>, int );

type parameters 和 Nontype parameters共存的情况
template <typename Type, int size>
Type min( Type (&r_array)[size] );

template <class Type, int size>
class Buffer;

只有Nontype parameters 的情况
template<CK_ULONG u> struct type_trait;
template<CK_ULONG> struct type_trait;    // u可以被省略,就如同函数参数在声明的时候可以被省略一样,但真正用的时候,不能省略。

有template type parameter的情况
template <class Type, template<class> class seq>    // U参数被省略
class Buffer;

template <class Type, template<class U> class seq> // 两个参数都未被省略
class Buffer;

template <class Type, template<class, size_t> class seq> // 两个参数都被省略
class Buffer2;

template <class Type, template<class U, size_t nSize> class seq> // 两个参数都未被省略
class Buffer2;

注释:声明一个template时候,和声明一个函数一样,Parameter可以被省略,只保留类型,如:
template <typename , unsigned long > // Type 和 n被省略。
class QueueItem;
 
template <class Type, unsigned long n>
class QueueItem {
public:
    QueueItem( const Type & i);
 
private:
    Type item;
    QueueItem *next;
};
 
template <class Type, unsigned long n>
QueueItem<Type, n>::QueueItem(const Type & i)
{
    item = i;
}
当然,模板也可以有默认参数:

template <class Type = int, int size = 30>
class Buffer;

Template 基础知识点

下面是总结自C++ Primer:

常见错误

template <class Type>
Type min1( Type a, Type b )
{
    // C++ Primer :
错误
, 重新声明模板参数 Type
    //
但是我使用VS2010编译通过,只是给出了一个警告
    typedef double Type;
    Type tmp = a < b ? a : b;
    return tmp;
}

// Type
重复使用。 就如同参数名不能一样。一样了编译器就无法识别了。
//
所以判断一个语法是否正确的通法是:是否能够造成编译器无法分析
template <class Type, class Type>
Type min2( Type a, Type b);

// typename or class
关键字不能省略。
template <typename T, U>
T sum( T*, U );

//
有时,编译器无法区分出是类型以及不是类型的表达式
//
如果编译器在模板定义中遇到表达式Parm::name Parm 这个模板类型参数代表了一个类,
//
那么name引用的是Parm的一个类型成员吗?
//
编译器不知道name是否为一个类型, 因为它只有在模板被实例化之后才能找到Parm
//
表示的类的定义. 为了让编译器不为之混淆,你必须显示的告诉编译器它是否是个类型表达式,
//
使用typename可以做到,所以在我转的另一篇的typename的用法中提到,typename用在模板中,目的是为了帮助编译器区分它是个类型,
//
否则编译器默认将其看做一个成员对待。
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
    Parm::name * p;        //
这是一个指针声明还是乘法乘法?
    typename Parm::name * p;    // ok:
指针声明
}

// ok:
关键字跟在模板参数表之后
template <typename Type>
inline
Type min( Type, Type );

//
错误: inline 指示符放置的位置错误
inline
template <typename Type>
Type min( Array<Type>, int );

//
正确: inline 指示符放置的位置
template <typename Type>
inline Type min( Array<Type>, int );

函数模板实例化
如果没有显示指定模板参数,这个过程是编译器隐式发生的,它可以被看作是函数模板调用或取函数模板的地址的副作用
在下面的程序中min()被实例化两次:一次是针对5 个int 的数组类型,另一次是针对6 个double 的数组类型。
template <typename Type, int size>
Type min( Type (&r_array)[size] );
 
// size 没有指定——ok
// size = 初始化表中的值的个数
int ia[] = { 10, 7, 14, 3, 25 };
double da[6] = { 10.2, 7.1, 14.5, 3.2, 25.0, 16.8 };
 
// 为5 个int 的数组实例化min()
// Type => int, size => 5
int i = min( ia );
 
// 为 6 个 double 的数组实例化 min()
// Type => double, size => 6
double d = min( da );

函数模板实参推演

为了判断用作模板实参的实际类型和值编译器需要检查函数调用中提供的函数实参的类型。在我们的例子中ia 的类型(即5 个int 的数组)和da 的类型(即6 个double 的数组)被用来决定每个实例的模板实参。用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演(template argument deduction)。

我们也可以不依赖模板实参推演过程,而是显式地指定模板实参。(需要调用者来明确)

函数模板在它被调用或取其地址时被实例化。在下面的例子中指针pf 被函数模板实例的地址初始化,编译器通过检查pf 指向的函数的参数类型来决定模板实例的实参。

template <typename Type, int size>
Type min( Type (&p_array)[size] ) { /* ... */ }
 
// pf 指向 int min( int (&)[10] )
int (*pf)(int (&)[10]) = &min;
Type 的模板实参为int, size 的模板实参为10。 被实例化的函数是min(int(&)[10]),指针pf 指向这个模板实例。
在取函数模板实例的地址时,必须能够通过上下文环境为一个模板实参决定一个惟一的类型或值。如果不能决定出这个惟一的类型或值,就会产生编译时刻错误,例如:
template <typename Type, int size>
Type min( Type (&r_array)[size] ) { /* ... */ }
 
typedef int (&rai)[10];
typedef double (&rad)[20];
void func( int (*)(rai) );
void func( double (*)(rad) );
 
int main() {
    // 错误: 哪一个 min() 的实例?
    func( &min );
}

因为函数func()被重载了,所以编译器不可能通过查看func()的参数类型,来为模板参数Type 决定惟一的类型,以及为size 的模板实参决定一个惟一值。这种情况,可以使用显示调用: func<int, 10>(&min)这种方式。

C++ Primer讲述了模板实参推演的过程,我不建议去记忆和学习。因为曾经看过。看过多少遍,基本上就忘过多少遍。本人比较笨,希望能理解。呵呵。因为你知道了其原理和不知道其原理其实没有多大区别 ,在实际运用当中,很少需要你去了解那么深刻。

显式指定(explicitly specify)函数模板实参

不依靠编译器,显式的实例化模板
// min5( unsigned int, unsigned int ) 被实例化
min5< unsigned int >( ui, 1024 );
 
// T1 不出现在函数模板参数表中
// 问题:T1 的模板实参不能从函数实参中被推演出来
template <class T1, class T2, class T3>
T1 sum( T2, T3 );
 
// 错误: T1 不能被推演出来
ui_type loc1 = sum( ch, ui );
 
// ok: 模板实参被显式指定
// T1 和 T3 是unsigned int, T2 是 char
ui_type loc2 = sum< ui_type, char, ui_type >( ch, ui );
在显式特化(explicit specification)中,我们只需列出不能被隐式推演的模板实参,如同缺省实参一样,我们只能省略尾部的实参例,例如:
// ok: T3 是 unsigned int
// T3 从 ui 的类型中推演出来
ui_type loc3 = sum< ui_type, char >( ch, ui );
 
// ok: T2 是 char, T3 是 unsigned int
// T2 和 T3 从 pf 的类型中推演出来
ui_type (*pf)( char, ui_type ) = &sum< ui_type >;
 
// 错误: 只能省略尾部的实参
ui_type loc4 = sum< ui_type, , ui_type >( ch, ui );

类模板

声明
template <class T>
class QueueItem;
 
template <class Type, int size>
class Buffer;
 
// 模板实例没有指定size的大小时,默认为1024
template <class Type, int size=1024>
class Buffer;
定义
template <class Type>
class QueueItem {
public:
    QueueItem( const Type & );
private:
    Type item;
    QueueItem *next;
};
在类模板定义中吗,类模板的名字(QueueItem)相当于是(QueueItem<Type>)的缩写。这种简写形式只能被用在类模板QueueItem 自己的定义中, 以及在类模板定义之外出现的成员定义中。

类模板实例化

从通用的类模板定义中生成类的过程被称为模板实例化(template instantiation)。类模板定义指定了怎样根据一个或多个实际的类型或值的集合来构造单独的类。Queue的类模板定义被用作"Queue类的特定类型的实例"的自动生成模板。

Queue<int> qi;

一个针对int 型对象的Queue 类就被从通用类模板定义中创建出来。所以,是实例的类型越多,被编译器实例化出来的类就越多,你的程序也就越庞大。所以,程序大小有要求的,最好不要用模板。
与函数模板实例的模板实参不同的是,根据类模板实例被使用的上下文环境,编译器无法推断出类模板实例的模板实参

Queue qs; // 错误: 哪一个模板实例?

只有当代码中使用了类模板的一个实例的名字,并且上下文环境要求必须存在类的定义时,这个类模板才被实例化。并不是每次使用一个类都要求知道该类的定义。 如:

class Matrix;
Matrix *pm; // ok: 不需要类 Matrix 的定义
void inverse( Matrix & ); // ok 也不需要
 
// Queue<int> 没有为其在 foo() 中的使用实例化
void foo( Queue<int> &qi )
{
    Queue<int> *pqi = &qi;
    // ...
}

所以声明一个类模板实例的指针和引用不会引起类模板被实例化

定义一个类的对象时需要该类的定义。例如在下面的例子中,obj1 的定义就是错的。这个对象的定义要求让编译器知道Matrix 的大小,以便为obj1 分配正确的内存数

class Matrix;
Matrix obj1; // 错误: Matrix 没有被定义
 
class Matrix { ... };
Matrix obj2; // ok

如果一个对象的类型是一个类模板的实例,那么当对象被定义时,类模板也被实例化

Queue<int> qi; // Queue<int> 被实例化

如果一个指针或引用指向一个类模板实例,那么只有当检查这个指针或引用所指的那个对象时,类模板才会被实例化。

void foo( Queue<int> &qi )
{
    Queue<int> *pqi = &qi;
    // 因为成员函数被调用, 所以 Queue<int> 被实例化
    pqi->add( 255 );
    // ...
}

非类型参数的模板实参

绑定给非类型参数的表达式必须是一个常量表达式,也就是在编译时刻能够被计算出来。

// 这么定义是没问题的
template <int *ptr> class BufPtr { ... };
 
// 但是如果这么实例化就是不对的,因为模板实参不能在编译时刻被计算出来
BufPtr< new int[24] > bp;
template <int size> Buf{ ... };
template <int *ptr> class BufPtr { ... };
 
int size_val = 1024;
const int c_size_val = 1024;
Buf< 1024 > buf0;         // ok
Buf< c_size_val > buf1;     // ok
Buf< sizeof(size_val) > buf2;     // ok: sizeof(int)
BufPtr< &size_val > bp0;     // ok
 
// 错误:不能在编译时刻计算出来。
Buf< size_val > buf3;

类模板的成员函数

// 内部定义
template <class Type>
class Queue {
public:
    // inline 构造成员函数
    Queue() : front( 0 ), back ( 0 ) { }
    // ...
};
 
// 外部定义
template <class Type>
class Queue {
public:
    Queue( );
private:
    // ...
};
template <class Type>
inline Queue<Type>::
    Queue( ) { front = back = 0; }

类模板的成员函数本身也是一个模板。标准C++要求这样的成员函数只有在被调用或者取地址时,才被实例化(标准C++之前的有些编译器在实例化类模板时,就实例化类模板的成员函数。)用来实例化成员函数的类型就是其成员函数要调用的那个类对象的类型。

Queue<string> qs;

对象qs 的类型是Queue<string> 。当初始化这个类对象时,Queue<string>类的构造函数被调用,在这种情况下用来实例化构造函数的模板实参是string。

常见错误

// 错误: 必须是 <typename T, class U> 或 <typename T, typename U>
template <typename T, U>
    class collection;
在下面的例子中,item 的类型不是double,它的类型是模板参数的类型:
typedef double Type;
template <class Type>
class QueueItem {
public:
    // ...
private:
    // item 不是 double 类型
    Type item;
    QueueItem *next;
};
 
// 错误: 重复使用名为Type 的模板参数
template <class Type, class Type>
    class container;
 
template <class Type>
class Queue {
public:
    // ...
private:
// 错误: QueueItem 不是一个已知类型
// 应该为QueueItem<Type>, 或者确定类型: QueueItem<int>之类的
    QueueItem *front;
};

书和编译器的一些分歧

template <class Type>
class QueueItem {
public:
    // ...
private:
// C++ Primer:错误: 成员名不能与模板参数 Type 同名
// 但我使用VS2008,VS2010,VS2012都可以编译通过,而且没有任何警告。
    typedef double Type;
    Type item;
    QueueItem *next;
};
 
// C++ Primer: ok: 考虑两个声明中的缺省实参
// 但我使用VS2008,VS2010,VS2012无法编译通过,编译器要求size也得需要默认值。
template <class Type = string , int size>
    class Buffer;

模板特化

模板特化是C++语言中,很奇特的一种技术。以它为理论基础,出现了元模板编程,大量运用在Boost模板库中。
关于为什么模板需要特化模板,这里不做详细说明,可以自行百度。特化从某种程度上说,是为了控制编译器实例化模板,让其更加精确的实例化出对应的模板。

如果将模板看成所有可能性,那模板特化就是部分可能性。特化限定了所有可能性的一部分特性,模板特化相当于整个模板的一个子集。 编译器实例化模板的过程,也属于模板特化的一种。而我们这里说的是由程序员显式的控制模板的特化。

模板特化分为全特化半特化(或叫偏特化)类模板和函数模板都能全特化,而函数模板无法半特化,只有类模板可以半特化。

模板全特化

全特化的格式为:
template<>
function/class classname<parameter instances…>
例如:
// 定义Compare模板
template<class T>
class Compare
{
public:
    static bool IsEqual(const T& lh, const T& rh)
    {
        return lh == rh;
    }
};
 
// 特化模板为: float
template<>
class Compare<float>
{
public:
    static bool IsEqual(const float& lh, const float& rh)
    {
        return abs(lh - rh) < 10e-3;
    }
};
 
// 特化模板为: double
template<>
class Compare<double>
{
public:
    static bool IsEqual(const double& lh, const double& rh)
    {
        return abs(lh - rh) < 10e-6;
    }
};

模板半特化

半特化的格式为:

template<parameters...>
function/class classname<parameters..., parameter instances…>

半特化,就是没有完全特化,只特化了部分模板参数,如:

template <class T, class Allocator>
class vector { // … // };
 
//将class T,特化为bool,而Allocator没有被特化。注意半特化格式的写法
template <class Allocator>
class vector<bool, Allocator> { //…//};
半特化同样要在函数或者类名后面同样要加上<>, 里面填入特化的参数,但没有特化的的参数,仍然使用原参数,但template的<>中的参数不为空,其中留下不特化的参数。

全特化,半特化和模板定义外观类似,不容易区分,容易使人混淆。这里有个区分技巧
是不是模板特化不是看template<>,而是看函数,或者类名后面是是否有'<实例参数>',如果有,那就是说明这个定义正在特例化。

上面的例子,将模板特化为实际的类型,(class T,特化为bool),还能特化"半成品",这个半成品主要就是指针和引用,如:

template <class _Iterator>
struct iterator_traits {
    typedef typename _Iterator::iterator_category iterator_category;
    typedef typename _Iterator::value_type        value_type;
    typedef typename _Iterator::difference_type     difference_type;
    typedef typename _Iterator::pointer             pointer;
    typedef typename _Iterator::reference         reference;
};

// specialize for _Tp*,
特化为一个指针

template <class _Tp>
struct iterator_traits<_Tp*> {
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                     difference_type;
    typedef _Tp*                        pointer;
    typedef _Tp&                        reference;
};

// specialize for const _Tp*
,特化为一个const 指针。
template <class _Tp>
struct iterator_traits<const _Tp*> {
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                     difference_type;
    typedef const _Tp*                    pointer;
    typedef const _Tp&                    reference;
};
一个完整的特化例子

#include <iostream>
using namespace std;
#include <string>
#include <vector>

//
一个简单的加法模板
template<class T>
class Addition
{
public:
    T fun( T a, T b )
    {
        return ( a + b );
    }
};

/*
**
特化
1
**
类模板特化(specialization
**
特化之前需要有类模板的声明
*/
template<>
class Addition<char *>
{
public:
    /*
    **
对每个函数和变量进行特化
    **
当然这里只有一个函数,哈
    */
    char * fun( char * a, char * b )
    {
        /*
        ** OMG
,正式的代码可不能这样写
        **
这儿只是个例子,将就,将就
        */
        strcat_s( a, 11, b );
        return a;
    }
};

/*
**
特化2
**
特化为引用,指针类型
**
这样的特化其实是一种"偏特化"
**
偏特化,半特化,局部特化,部分特化……翻译五花八门
**
还是记住英文比较好:partial specialization
**
在编码时,总纠结着是指针好呢,还是引用好呢,要加const乎?
**
有了这样的特化,确实方便甚多。
*/
template<class T>
class Addition<T &>
{
public:
    T & fun( T & a, T & b )
    {
        a = a + b;
        return a;
    }
};

/*
**
特化3
**
还可以这样特化
*/
template<class T>
class Addition< vector<T> >
{
public:
    vector<T> & fun( vector<T> & a, vector<T> & b )
    {
        /*
        **
假想着两个vector大小一样
        */
        for ( int i = 0; i < (int)a.size(); ++i )
        {
            a[i] += b[i];
        }
        return a;
    }
};

int main()
{
    Addition<int> A;
    cout << A.fun( 10, 20 ) << endl;

    //
将特化1的代码注释会怎么样?
    char str1[11] = "aaaaa";
    char str2[] = "BBBBB";
    Addition<char *> B;
    cout << B.fun( str1, str2 ) << endl;

    //
将特化2的代码注释会怎么样?
    string cs1 = "sssss";
    string cs2 = "ddddd";
    Addition<string &> C;
    cout << C.fun( cs1, cs2 ) << endl;

    //
特化3的例子
    vector<int> v1;
    v1.push_back( 10 );
    v1.push_back( 20 );

    vector<int> v2;
    v2.push_back( 100 );
    v2.push_back( 200 );

    Addition< vector<int> > D;
    D.fun( v1, v2 );

    for ( int i = 0; i < (int)v1.size(); ++i )
    {
        cout << v1[i] << " ";
    }
    cout << endl;


    return 0;
}

特化的目的是为了让编译器精确匹配。每种特化的结构体里的
typedef
都可能不一样。这样,在编译器这一层面上,就能实现一种多态的效果
,这种编程叫做模板元编程,可以参考《Boost 程序库探秘》第一章。