C++Template学习笔记之函数模板

来源:互联网 发布:域名在哪转让 编辑:程序博客网 时间:2024/04/30 04:09
函数模板提供了一种机制,通过它可以保留函数定义和函数调用的语义(在一个程序位置上封装了一段代码,确保在函数调用之前实参只被计算一次),而无需象宏方案那样绕过C++的强类型检查。如:

        template <class Type>
        Type min(Type a,Type b)
        {
              return a<b ? a : b;
         }

        关键字template总是放在模板的定义与声明的最前面。关键字后面是用逗号分隔的模板参数表,它用尖括号(<>)括起来。该列表是模板参数表,不能为空。模板参数分为:(1) 模板类型参数,代表一种类型;(2) 模板非类型参数,代表一个常量表达式。

        模板类型参数由关键字class或typename后加一个标识符构成。在函数的模板参数表中,这两个关键字的意义相同。它们表示后面的参数名代表一个潜在的内置或用户定义的类型。

        模板非类型参数由一个普通的参数声明构成。模板非类型参数表示该参数名代表了一个潜在的值,而该值代表了模板定义中的一个常量。如:

        template <class Type,int size>
        Type min(Type (&arr)[size]);

       规则:

        1. 如果在全局域中声明了与模板参数同名的对象、函数或类型,则该全局名将被隐藏。

        2. 在函数模板定义中声明的对象或类型不能与模板参数同名。

        3. 模板类型参数名可以被用来指定函数模板的返回位。

        4. 模板参数名在同一模板参数表中只能被使用一次,但可在多个函数模板声明或定义之间被重复使用。

        通过将关键字typename引入到C++中,我们可以对模板定义进行分析。为了分析模板定义,编译器必须能够区分出是类型以及不是类型的表达式。如(Parm代表一个类):

        template <class Parm,class U>
        Parm minus(Parm* array,U value)
        {
               Parm::name * P; // 这是一个指针声明还是乘法?答案是乘法
        }

        编译器不知道name是否为一个类型,因为它只有在模板被实例化之后才能找到Parm表示的类的定义。为了让编译器能够分析模板定义,用户必须指示编译器哪些表达式是类型表达式。告诉编译器一个表达式是类型表达式的机制是在表达式前加上关键字typename。如:

        template <class Parm,class U>
        Parm minus(Parm* array,U value)
        {
               typename Parm::name * P; // 这是指针声明
        }

        函数模板也可被声明为inline或extern。但应把指示符放在模板参数表后面,而不是在template前面:

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

        函数模板指定了怎样根据一组或更多实际类型或值构造出独立的函数。这个构造过程被称为模板实例化。这个过程是隐式发生的,它可以被看作是函数模板调用或取函数模板的地址的副作用。

        为了判断用作模板实参的实际类型和值,编译器需要检查函数调用中提供的函数实参的类型。用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演。我们也可以不依赖模板实参推演过程,而是显式地指定模板实参。

        在取函数模板实例的地址时,必须能够通过上下文环境为一个模板实参决定一个唯一的类型或值。如果不能决定出这个唯一的类型或值,就会产生编译时错误。如:

        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的模板实参决定一个唯一值。我们可以用一个强制类型转换显式地指出实参的类型来消除编译时错误:

        int main() {
                   //OK:强制转换指定实参类型
                  func( static_cast< double(*)(rad) >(&min) );
        }

        但是更好的方案是用显式模板实参。

        当函数模板被调用时,对函数实参类型的检查决定了模板实参的类型和值,这个过程被称为模板实参推演。

        其通用算法如下:

        1. 依次检查每个函数实参,以确定在每个函数参数的类型中出现的模板参数;

        2. 如果找到模板参数,则通过检查函数实参的类型,推演出相应的模板实参;

        3. 函数参数类型和函数实参类型不必完全匹配。下列类型转换可以被应用在函数实参上,以便将其转换成相应的函数参数的类型:
           (1). 左值转换;
           (2). 限定修饰转换;
           (3). 从派生类到基类类型的转换。假定函数参数具有形式T<args>、T<args>&或T<args>*,则这里的参数表args至少含有一个模板参数;
           (4). 如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同。

       在某些情况下编译器不可能推演出模板实参的类型。如笔记(2)中所举的例子,如果模板实参推演过程为同一模板实参推演出两个不同的类型,则编译器会给出一个错误,指出模板推演失败。

        在这种情况下,我们需要改变模板实参推演机制,使用显式指定(explicitly specify)模板实参。模板实参被显式指定在逗号分隔的列表中,用尖括号<>括起来,紧跟在函数模板实例的名字后面。如:

        template <class T>
            T min5( T, T ) {/* . . . */ }
        // min5( unsigned int, unsigned int ) 被实例化
        min5< unsigned int >( ui, 1024 );

       在这种情况下,模板实参表< unsigned int >显式地指定了模板实参的类型。第2个实参是1024,其类型是int。因为第2个函数参数的类型通过显式模板实参已被固定为unsigned int,所以第2个函数实参通过有序标准转换被转换成类型unsigned int。

        在显式特化中,我们只需列出不能被隐式推演的模板实参,如同缺省实参一样,我们只能省略尾部的实参。如:

        template <class T1,class T2,class T3>
                   T1  sum ( T2, T3) ;
        // 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>;
        // ERROR: 只能省略尾部的实参
        ui_type loc4 = sum<ui_type , , ui_type>(ch,ui);

        必须要指出,显式模板实参应该只被用在完全需要它们来解决二义性,或在模板实参不能被推演出来的上下文中使用模板实例时。

一、包含编译模式

        在包含编译模式下,我们在每个模板被实例化的文件中包含函数模板的定义,并且往往把定义放在头文件中,象内联函数所做的那样。如:

        // model1.h
        // 包含模式:模板定义放在头文件中
        template <typename Type>
              Type min( Type t1, Type t2 ) {
                        return t1 < t2 ? t1 : t2;
        }

        在每个使用min()实例的文件中都包含了该头文件,如:

        // 在使用模板实例之前包含模板定义
        #include "model1.h"
        int i, j;
        double dobj = min( i, j );

        该头文件可以被包含在许多程序文本文件中。这意味着编译器必须在每个调用该实例的文件中实例化min()的整型实例吗?不。该程序必须表现得好像min()的整型实例只被实例化一次。但是,真正的实例化动作发生在何时何地,要取决于具体的编译器实现。

二、分离编译模式

        在分离编译模式下,函数模板的声明被放在头文件中。在这种模式下,函数模板声明和定义的组织方式与程序中的非内联函数的声明和定义组织方式相同。如:

        // model2.h
        // 分离模式:只提供模板声明
        template <typename Type>  Type min( Type t1, Type t2 );

        // model2.c
        // 模板定义
        export  template <typename Type>
                Type min( Type t1, Type t2 ) {/* . . . */}

        使用函数模板min()实例的程序只需在使用该实例之前包含这个头文件:

        // user.c
        #include "model2.h"
        int  i, j;
        double d = min( i, j ); // OK: 用法,需要一个实例

        我们通过在模板定义中的关键字template之前加上关键字export,来声明一个可导出的函数模板。

三、显式实例化声明

        标准C++提供了显式实例化声明来帮助程序员控制模板实例化发生的时间。在显式实例化声明中,关键字template后面是函数模板实例的声明,其中显式地指定了模板实参。下例中提供了sum(int* , int)的显式实例化声明:

        template <typename Type>
              Type sum( Type op1, int op2 ) {/* . . . */} // 函数模板sum的定义必须给出
        // 显式实例化声明
        template int* sum< int* >( int*, int );

        该显式实例化声明要求用模板实参int*实例化模板sum()。对于给定的函数模板实例,显式实例化声明在一个程序中只能出现一次。


        我们并不总是能够写出对所有可能被实例化的类型都是最合适的函数模板。在某些情况下,我们可能想利用类型的某些特性,来编写一些比模板实例化的函数更高效的函数。在有些时候,一般性的模板定义对于某种类型来说并不适用,这时我们必须为函数模板实例化提供特化的定义。

        在模板显式特化定义(explicit specialization definition)中,先是关键字template和一对尖括号<>,然后是函数模板特化的定义。该定义指出了模板名、被用来特化模板的模板实参,以及函数参数表和函数体。如:

        // max.h
        template <class Type>
           Type Max(Type t1,Type t2)
          {
                  return (t1>t2 ? t1 : t2);
           }

         // const char*显式特化:覆盖了来自通用模板定义的实例

         typedef const char *PCC;
         template<> PCC Max< PCC >( PCC s1, PCC s2 )
         {
                 return (strcmp(s1,s2)>0 ? s1 :s2);
          }

        // user.c
        int main()
        {
            // 调用实例:int  Max< int >( int, int );
            int  i = Max( 10, 5 );
            // 调用显式特化:const char* Max< const char* >( const char*, const char* );
            const char *p = Max( "hello", "world" ); 
            cout<<"i: "<<i<<" "<<"p: "<<p<<endl;

            return 0;
          }


       函数模板可以被重载。如:

        // 类模板Array的定义
        template <typename Type>
                 class Array { /* . . . */ };
        // main()的三个函数模板声明
         template <typename Type>
                     Type min( const Array<Type>&, int );  // #1
         template <typename Type>
                     Type min( const Type*, int );                 // #2
          template <typename Type>
                     Type min( Type, Type );                         // #3

        调用:

        int main(){
                     Array<int> iA(1024); // 类实例
                     int  ia[1024];

                     // Type==int; min( const Array<int>&, int )
                     int  ival0 = min( iA, 1024 );

                     // Type==int; min( const int*, int )
                     int  ival1 = min( ia, 1024 );

                     // Type==double; min( double, double )
                     double  dval0 = min( sqrt( iA[0] ), sqrt( ia[0] ) );

                     return 0;
         }

        在某些情况下,即使对于一个函数调用,两个不同的函数模板都可以实例化,但是该函数调用仍然可能不是二义的。如:

        template <typename Type>
               Type sum( Type*, int );
        template <typename Type>
               Type sum( Type, int );
        int  ia[1024];

        // Type==int; sum<int>( int*, int ); or
        // Type==int*; sum<int*>( int*, int); ??
        int  ival1 = sum<int>( ia, 1024 );

        上面的调用没有二义性,该模板是用第一个模板定义实例化的。为该实例选择的模板函数是最特化的(most specialized)。因此,Type的模板实参是int而不是int*。

        一个模板要比另一个更特化,两个模板必须有相同的名字、相同的参数个数,对于不同类型的相应函数参数,如上面的T*和T,一个参数必须能接受另一个模板中相应参数能够接受的实参的超集。 
原创粉丝点击