C++模板

  模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。

  模板是一种对类型进行参数化的工具,通常有两种形式:函数模板和类模板

  (1)、函数模板针对仅参数类型不同的函数;

  (2)、类模板针对仅数据成员和成员函数类型不同的类。

  使用模板的目的就是能够让程序员编写与类型无关的代码。注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。


 一、函数模板通式

template <class 形参名,class 形参名,......> 
返回类型 函数名(参数列表){
  函数体
}

  其中template和class是关键字,class可以用typename 关见字代替,在这里typename 和class没区别,

  <>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。

 模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。

    (模板特化,模板特化必须有模板的前向声明或定义-(前置模板声明),即标准的模板定义,才能进行!,见5。

  标准模板定义中,类名后面不能带任何的模板形参。 )


 二、类模板通式

template<class  形参名,class 形参名,…> 
class 类名{
     ...
 };
  • 1.  类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空。 

            一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如

template<class T> 
class A{
    public: T a; T b; 
    T hy(T c, T &d);
};

  在类A中声明了两个类型为T的成员变量a和b,还声明了一个返回类型为T带两个参数类型为T的函数hy。

  • 2.  类模板对象的创建:比如一个模板类A,则使用类模板创建对象的方法为A<int> m;在类A后面跟上一个<>尖括号并在里面填上相应的类型,这样的话类A中凡是用到模板形参的地方都会被int 所代替。当类模板有两个模板形参时创建对象的方法为A<int, double> m;类型之间用逗号隔开。
  • 3.  对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。比如A<2> m;用这种方法把模板形参设置为int是错误的(编译错误:error C2079: 'a' uses undefined class 'A<int>')。

        类模板形参不存在实参推演的问题,也就是说不能把整型值2推演为int 型传递给模板形参。要把类模板形参调置为int 型必须这样指定A<int> m。

  • 4.  在类模板外部定义成员函数的方法为:
template<模板形参列表> 
函数返回类型 类名<模板形参名>::函数名(参数列表){
    函数体
}

  比如有两个模板形参T1,T2的类A中含有一个void h()函数,则定义该函数的语法为:

template<class T1, class T2> 
void A<T1,T2>::h(){
}

  注意:当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致。

  • 5.  再次提醒注意:模板的声明或定义只能在全局,命名空间或类范围内进行。

         即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。


三、模板的形参

  有三种类型的模板形参:类型形参,非类型形参。

  • 1、类型形参

  • 类型模板形参:类型形参由关见字class或typename后接说明符构成,如 

    template<class T> 
    void h(T a){
    };

    其中T就是一个类型形参,类型形参的名字由用户自已确定。模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。

  • 2、非类型形参

    • 2.1  模板的非类型形参也就是内置类型形参,如 template<class T, int a> class B{};其中int a就是非类型的模板形参。
    • 2.2  非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。
    • 2.3  非类型模板的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针是正确的。
    • 2.4  调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。
    • 2.5  注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。
    • 2.6 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。
    • 2.7   sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。
    • 2.8   当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template <class T, int a> class A{};如果 有int b,这时A<int, b> m;将出错,因为b不是常量,如果const int b,这时A<int, b> m;就是正确的,因为这时b是常量。
    • 2.9   非类型形参一般不应用于函数模板中,比如有函数模板template<class T, int a> void h(T b){},若使用h(2)调用会出现无法为           非类型形参a推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用h<int, 3>(2)这样就把非类型形参a设置为整数           3。显示模板实参在后面介绍。
    • 2.10 非类型模板形参的形参和实参间所允许的转换

      1、允许从数组到指针,从函数到指针的转换。如:template <int *a> class A{}; int b[1]; A<b> m;即数组到指针的转换
      2、const修饰符的转换。如:template<const int *a> class A{}; int b; A<&b> m; 即从int *到const int *的转换。
      3、提升转换。如:template<int a> class A{}; const short b=2; A<b> m; 即从short到int 的提升转换
      4、整值转换。如:template<unsigned int a> class A{}; A<3> m; 即从int 到unsigned int的转换。
      5、常规转换。


四、类模板的默认模板形参

  • 1  可以为类模板类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。
  • 2  类模板的类型形参默认值形式为:template<class T1, class T2=int> class A{};为第二个模板类型形参T2提供int型的默认值。
  • 3  类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,比如       template<class T1=int, class T2>class A{};就是错误的,因为T1给出了默认值,而T2没有设定。
  • 4  在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型。比如
template<class T1, class T2=int> 
class A{
    public: void h();
}; 
定义方法为
template<class T1,class T2> 
void A<T1,T2>::h(){
}


五、模板的特化

C++中的class也可以类型参数化,其中容器类是极具这一特征的。对于模板类的基本定义和使用,可以参考STL。
  • 1.  模板的特化- 全特化

  这里可以先将类模板特化与面向对象中的多态进行一个简单的比较,这样可以便于我们对它的理解,也同样有助于指导我们在实际的开发中应用这一C++技巧。

  众所周知,对于多态而言,提供的是统一的接口和不同的实现类实例,其最终的行为将取决于实现类中的实现,相信每一个有面向对象基础的开发者对于这一概念并不陌生。

  而模板特化则要求必须提供一个标准的模板类(等同于多态中的接口),与此同时再为不同的类型提供不同的特化版本。

  在模板特化类中,我们首先需要保证类名是相同的,只是模板参数不再使用抽象的类型,而是直接使用具体的类型来实例化该模板类。

  在调用时,编译器会根据调用时的类型参数自动选择最为合适的模板类,即如果有特化模板类中的类型参数和当前调用类型相匹配的话,则首选该特化模板类,否则选择最初的标准模板类

   #include <stdio.h>
    #include <string.h>
    
    //1. 这里我们先声明了一个通用类型的模板类。这里要有类型参数必须包含hashCode()方法。
    //否则,该类型在编译期实例化时将会导致编译失败。
    template <typename T>
    class CalcHashClass { //该类为标准模板类(等同于多态中的接口)
    public:
        CalcHashClass(T const& v) : _value(v) {
        }
        int hashCode() {
            printf("This is 'template <typename T> class CalcHashClass'.
");
            return _value.hashCode() + 10000;
        }
    private:
        T _value;
    };
    
    //2. int类型实例化特化模板类。
    template <>
    class CalcHashClass<int> {
    public:
        CalcHashClass(int const& v) : _value(v) {
        }
        int hashCode() {
            printf("This is 'template <> class CalcHashClass<int>'.
");
            return _value * 101;
        }
    private:
        int _value;
    };
    
    //3. const char*类型实例化的特化模板类
    template<>
    class CalcHashClass<const char*> {
    public:
        CalcHashClass(const char* v) {
            _v = new char[strlen(v) + 1];
            strcpy(_v,v);
        }
        ~CalcHashClass() {
            delete [] _v;
        }
        int hashCode() {
            printf("This is 'template <> class CalcHashClass<const char*>'.
");
            int len = strlen(_v);
            int code = 0;
            for (int i = 0; i < len; ++i)
                code += (int)_v[i];
            return code;
        }
    
    private:
        char* _v;
    };
    
    //4. 辅助函数,用于帮助调用者通过函数的参数类型自动进行类型推演,以让编译器决定该
    //实例化哪个模板类。这样就可以使调用者不必在显示指定模板类的类型了。这一点和多态有
    //点儿类似。
    template<typename T>
    inline int CalcHashCode(T v) {
        CalcHashClass<T> t(v);
        return t.hashCode();
    }
    
    //5. 给出一个范例类,该类必须包含hashCode方法,否则将造成编译错误。
    class TestClass {
    public:
        TestClass(const char* v) {
            _v = new char[strlen(v) + 1];
            strcpy(_v,v);
        }
        ~TestClass() {
            delete [] _v;
        }
    public:
        int hashCode() {
            int len = strlen(_v);
            int code = 0;
            for (int i = 0; i < len; ++i)
                code += (int)_v[i];
            return code;
        }
    private:
        char* _v;
    };
    
    int main() {
        TestClass tc("Hello");
        CalcHashClass<TestClass> t1(tc);
        printf("The hashcode is %d.
",t1.hashCode());
        //这里由于为模板类TestClass提供了基于int类型的模板特化类,因此编译器会自动选择
        //更为特化的模板类作为t2的目标类。
        CalcHashClass<int> t2(10);
        printf("The hashcode is %d.
",t2.hashCode());
    
        //在上面的示例中,我们通过显示的给出类型信息以实例化不同的模板类,这是因为模板类
        //的类型信息是无法像模板函数那样可以通过函数参数进行推演的,为了弥补这一缺失,我们可以
        //通过一个额外的模板函数来帮助我们完成这一功能。事实上,这一技巧在Thinking in Java中
        //也同样给出了。
        printf("Ths hashcode is %d.
",CalcHashCode(10));
        printf("Ths hashcode is %d.
",CalcHashCode("Hello"));
        return 0;
    }
    //This is 'template <typename T> class CalcHashClass'.
    //The hashcode is 10500.
    //This is 'template <> class CalcHashClass<int>'.
    //The hashcode is 1010.
    //This is 'template <> class CalcHashClass<int>'.
    //Ths hashcode is 1010.
    //This is 'template <> class CalcHashClass<const char*>'.
    //Ths hashcode is 500.

   通过上面的示例可以看出,模板特化是依赖于编译器在编译期动态决定该使用哪个特化类,或是标准模板类的。相比于多态的后期动态绑定,该方式的运行效率更高,同时灵活性也没有被更多的牺牲。

  • 2.  偏特化 - 部分特化

  有的书中将其翻译成模板偏特化,或者是模板的局部特化,但含义都是相同的。为了便于理解,我们可以将上面的模板特化称为模板全部特化,即模板类的类型参数全部被特化了。顾名思义,模板部分特化只是将其中一部分类型参数进行了特化声明,因此也可以将模板特化视为模板部分特化的一种特殊形式。由于应用场景基本相同,因此下面的代码将仅仅给出最基本的示例和注释说明,以帮助大家熟悉他的语法即可:
//1. 标准模板类。
    template<typename T1, typename T2>
    class MyClass {
        ... ...
    };
    //2. 两个模板参数具有相同类型的部分特化类。
    template<typename T>
    class MyClass<T,T> {
        ... ...
    }
    //3. 第二个类型参数是int
    template<typename T>
    class MyClass<T,int> {
        ... ...
    }
    //4. 两个模板参数都是指针。
    template<typename T1,typename T2>
    class MyClass<T1*,T2*> {
        ... ...
    }
    //5. 两个模板参数都是相同类型的指针。
    template<typename T>
    class MyClass<T*,T*> {
        ... ...
    }
    //6. 调用示例代码。
    int main() {
        MyClass<int,float> c1;         //调用MyClass<T1,T2>
        MyClass<float,float> c2;    //调用MyClass<T,T>
        MyClass<float,int> c3;      //调用MyClass<T,int>
        MyClass<int*,float*> c4;    //调用MyClass<T1*,T2*> 
        MyClass<int*,int*> c5;      //调用MyClass<T*,T*>
        return 0;
    }


六 、 typename的另一种使用方式

  在此之前,我们了解到的有关该关键字的用途更多是针对模板参数的定义。而这里介绍的使用方式则有些不同,主要区别是
typename的这种使用方式用于定义或提示编译器,其后
修饰标识符是模板参数中的类型标识符,而不是普通的静态类成员。见以下代码示例和关键性注释。
    #include <stdio.h>

    template<typename T>
    class MyTestClass {
    public:
        //这里的typename是用于通知编译器,其后的MyType是模板T定义的内部类型,从这个代码示例
        //中可以看出,不管是在函数参数、返回值还是定义成员变量定义,都要遵守这一语法规则。
        MyTestClass(typename T::MyType p) : _p(p) {
        }
        typename T::MyType GetData() const {
            return _p; 
        }
    public:
        typename T::MyType _p;
    };
    //在该类中,由于后面的调用需要将其用作MyTestClass的模参,因此他必须typedef名字为MyType的
    //内部类型,否则将会导致编译失败。
    class TemplateParamClass {
    public:
        typedef const char* MyType;
    };
    
    int main() {
        MyTestClass<TemplateParamClass> t("Hello");
        printf("The data is %s.
",t.GetData());
        return 0;
    }
 






参考:

 [1]. Step By Step(C++模板类) : http://www.cnblogs.com/stephen-liu74/archive/2012/08/22/2599400.html

原文地址:https://www.cnblogs.com/yaozhongxiao/p/4391307.html