导航

c++模板

Posted on 2017-11-22 19:57  困或  阅读(3738)  评论(0编辑  收藏  举报

1.模板的作用

  [1]模板分为函数模板和类模板,函数模版是用来生成函数的实例,类模版是用来生成类的实例。

  [2]一个模版就是一个类或函数的蓝图或者说是公式。当我们调用template时,编译器会使用实参的类型来确定绑定到模版参数T上的类型,之后编译器利用推断出的模版参数来实例化一个特定版本的函数,这个过程被称之为实例化。

  [3]编译器遇到一个模版的定义时,并不会产生代码,只有当我们实例化出模版的一个特定版本的时候,编译器才会产生代码。

  [4]需要保证传递给模版的实参支持模版的所有操作(例如传入的是addr类型,则在类模板中操作"<"运算符时,addr类型需要实现重载这个运算符),以及这些操作在模版中正确工作。

  函数模板:一个函数模版就是一个公式,可用来生成指定类型的函数版本,例如需要编写一个函数比较两个值,如果没有模板,则需要为各种类型(例如int,double,string等)各自编写一个比较函数。

  类模板:类模板是用来生成类的模型,和函数模板不同的是,编译器不能为类模板推断模板参数类型,必须提供实参列表。这个是显然的,因为函数是直接调用,传入的参数可以推断出类型,但是类需要定义,如果不指定类型,则是不行的。

2.函数模板

 1 #include <iostream>  
 2 #include <stdio.h>  
 3 #include <string.h>  
 4 #include <vector>  
 5 using namespace std;  
 6 
 7 template <typename T> int compare(const T &v1, const T &v2){
 8     if(v1 < v2) 
 9         return -1;
10     if(v2 < v1)
11         return 1;
12     return 0;
13 }
14 
15 template <typename T> inline int inline_compare(const T &v1, const T &v2){
16     if(v1 < v2) 
17         return -1;
18     if(v2 < v1)
19         return 1;
20     return 0;
21 }
22 
23 template <char len> int str_compare(const char (&v1)[len], const char (&v2)[len]){  //len就是一个非类型参数
24     return strcmp(v1, v2);
25 }
26 
27 int main(int argc, char *argv[])  
28 {  
29     vector<int> ve1(10, 1), ve2(10, 2);
30 
31     printf("compare:           %d\n", compare(3, 2));                    //常规使用
32     printf("compare float:     %d\n", compare<int>(30.1, 20.1));         //显式使用,这里会把参数强转为int
33     printf("compare vector:    %d\n", inline_compare(ve1, ve2));
34     printf("inline_compare:    %d\n", inline_compare(6, 7));
35     printf("str_compare:       %d\n", str_compare("hello", "abcde"));
36     return 0;
37 }  

  [1]<typename T>是模板参数列表,类似函数参数列表,函数参数列表定义了特定类型的局部变量,在运行时,提供实参来初始化形参,而模板参数列表定义了模板函数中用到类型或值,使用时,也是用实参(参数是类型或者值)来绑定到模板参数上。这意思就是模板的参数包括两个东西,首先是根据传入的参数类型实例化出实例,然后用这个实例执行传入的参数值。typename可以用class代替,没有任何区别。

  [2]模板参数一般都是用typename定义的类型,也可以用非类型参数,非类型参数的意思就是说这个参数不是传入类型,而是传入一个值,非类型参数的值的类型可以是整型、指针或引用,总之这个值必须是常量表达式(其实就是和普通函数中最常规的传参一样),并且这个参数需要有静态生存期(全局变量有静态生存期)。上面例子的第23行。

  [3]当调用一个函数模板时,编译器会用函数实参来推断模板实参,然后用推断出的模板参数来实例化一个特定版本的函数。就是说传入不同的类型编译器实例化出不同的特定函数,模板的使用可以是隐式调用(compare(30, 20)),也可以显式的调用(compare<int>(30.1, 20.1)),这个的意思就是实例化可以是让编译器根据参数自行推断出实例,也可以指定参数类型确定实例,但是有些情况时必须要指定的,比如返回值的类型和参数列表中的类型都不同时,参考下面的例子:

/*
    这里的T_RET类型只在返回的时候使用,所以使用的时候必须指定T_RET的类型。
    这意思就是说,例如:
        int sum = fun(1, 2);
    这样实例化fun时,T_RET和T类型都没有显式的传入类型,但是编译器可以根据参数1、2推断出T为int型,而T_RET却无法根据传入的参数推断出是什么类型。
*/
template <typename T_RET, typename T> T_RET fun(const T& v1, const T& v2)
{
    return v1 + v2;
}

int sum = fun<int, int>(1, 2);//正确,显示调用,推断出的实例是fun(int, int)
int sum = fun<int>(1, 2);     //正确,推断出的实例是fun(int, int),T_RET是指定的int类型,T是根据实参1、2推断的
int sum = fun(1, 2);          //错误,编译器无法推断出T_RET是什么类型

  [4]因为上面的上面的例子用"<"和">"比较两个值,所以传入的类型需要实现这两个运算符。

  [5]函数模版可以声明为inline或是constexpr,置于模版参数列表之后,函数返回类型之前。上面的上面的例子第15行。

3.类模板

  [1]一个类模板的每个实例都会形成一个独立的类,类和类之间没有任何关联。

  [2]和普通类一样,可以在类内部和外部定义类模板的成员函数,模板内的成员函数隐式声明为inline函数,默认情况下,对于一个实例化的类模板,其成员只有在使用时才被实例化。

  [3]在类模板自己的作用域中,可以直接使用模板名而不提供实参。(就是说在类模板作用域中,my_vector<T> 和my_vector 是可以替换的)

  [4]类模板中的static成员,这些成员由相同类型的实例共享,例如my_vector<int> i1,i2,i3; 则i1、i2、i3共享类中的static成员。

  [5]在模板中使用类的类型成员,这个意思就是在模板里面,要使用传入的类型参数里面的类型,例如传入的参数是myclass(定义:my_vector<myclass> cl_vector;),如果要用myclass类里面的类型(例如myclass里面定义了一个类mylittleclass类)来定义一个变量,则方法是typename T::mylittleclass *p;这里主要是用到了typename关键字。

  [6]模板的默认实参,模板可以设置默认实参,template <class T = int> class my_vector{},使用时可以不指定实参,但是需要写上尖括号(my_vector<> int_vector)。

  [7]类模板的成员模板,意思就是类里面的函数也是一个模板,成员模板不能是虚函数,参考下面的例子:

 1 #include <iostream>  
 2 #include <stdio.h>  
 3 #include <string.h>  
 4 #include <vector>  
 5 using namespace std;  
 6 
 7 template <typename T, int size=100> class my_vector{
 8 public:
 9     void dump_int();
10     void dump_str();
11 
12     my_vector() {
13         m_size = size;
14     }
15     template <typename It> my_vector(It a, It b);  //构造函数是一个函数模板
16 
17 private:
18     int m_size;
19     vector<T> m_data;
20 };
21 
22 //构造函数的实现 23 template <typename T, int size> template <typename It> my_vector<T, size>::my_vector(It a, It b){ 24 while(a != b){ 25 m_data.push_back(*a); 26 a++; 27 } 28 } 29 30 template <typename T, int size> void my_vector<T, size>::dump_int() 31 { 32 typename vector<T>::iterator iter; 33 for (iter=m_data.begin();iter!=m_data.end();iter++) 34 printf("\t%d\n", *iter); 35 36 return ; 37 } 38 39 template <typename T, int size> void my_vector<T, size>::dump_str() 40 { 41 typename vector<T>::iterator iter; 42 for (iter=m_data.begin();iter!=m_data.end();iter++) 43 printf("\t%s\n", (*iter).c_str()); 44 45 return ; 46 } 47 48 int main(int argc, char *argv[]) 49 { 50 vector<int> ia; 51 ia.push_back(1000); 52 ia.push_back(2000); 53 ia.push_back(3000); 54 ia.push_back(4000); 55 my_vector<int> int_vector10(ia.begin(), ia.end()); 56 printf("int_vector10:\n"); 57 int_vector10.dump_int(); 58 59 vector<string> is; 60 is.push_back("aa"); 61 is.push_back("bb"); 62 is.push_back("cc"); 63 is.push_back("dd"); 64 my_vector<string> int_vector11(is.begin(), is.end()); 65 printf("int_vector11:\n"); 66 int_vector11.dump_str(); 67 return 0; 68 }

输出:

 4.相关特性

  [1]控制实例化的时机,因为当模板使用的时候才会实例化,如果实例化大量相同模板会影响性能,所以可以提前通过显式实例化来避免这种情况。只有在模板定义时才会实例化,这个和普通的函数声明和定义是一样的。但是有地方要注意,定义的时候的实例化会实例化所有成员,包括inline函数(正常的实例化是只实例化用到的成员),这就要求在实例化定义中,所用类型必须能作用于模版的所有成员函数(例如定义了template class my_vector<int>,则int类型要满足my_vector中所有的成员函数处理)。

 1 template_build.cpp
 2     template class my_vector<int>;                        //定义,并且会实例化
 3     template int compare(const int &, const int &);        //定义,并且会实例化
 4 
 5 application.cpp
 6     extern template class my_vector<int>;                
 7     extern template int compare(const int &, const int &);
 8 
 9     //这时候使用,编译器就不用再实例化了
10     my_vector<int> int_v;
11     compare(1, 2);

  [2]函数模板参数的类型转换,正常使用模板时,会根据传入的参数类型实例化出不同的实例,如果传入的类型不匹配则会发生编译错误(下面例子中的fun_c(s1, s2); )。但是有两种情况不会发生编译错误,一种是非const对象的引用或者指针(实参)到const对象的引用或者指针(形参),另一种是形参不是引用类型时,数组名可以转为一个指向首元素的指针,函数名可以转为一个指向函数类型的指针。这个意思就是说,定义的模板函数的参数是const T &,则可以传给这个参数 T &类型(自动转为const)。定义的模板函数的参数是int *,则可以传给这个参数array(array定义:int array[10];)。另外对于下面的fun_a(s1, s2);,这个是一个值传递,跟const没有任何关系,因为函数不操作s1,s2本身,操作的是参数副本。

      对于模板参数T &,则传入的实参必须是一个左值(例如不能是一个常量),如果实参是const则推断出的形参T也是const。

      对于模板参数const T &,则传入的实参可以是任意值,当传入的实参是const时,由于模板形参T本身就是const,所以推断出的T是不带const的。

        以上意思就是说形参为const引用的可以传入常量;传入的参数是什么类型,推断出的T就是什么类型,当形参本身就是const时,推断出的T不带const;非const的引用的实参可以自动转为const引用的形参,而反过来就不行。

 1 template <typename T> T fun_a(T v1, T v2) {return v1;};
 2 template <typename T> T fun_b(const T& v1, const T& v2){return v1;};
 3 template <typename T> T fun_c(T&v1, T&v2){return v1;};
 4 
 5 int main(){
 6     string s1("s1");
 7     const string s2("s2");
 8 
 9     fun_a(s1, s2);          //这个会实例化出fun_a(string, string); 传值
10     fun_a(s2, s2);          //这个还是会实例化出fun_a(string, string); 因为这是值传递,const是忽略的
11 
12     fun_b(s1, s2);          //非const可以自动转为const
13     fun_b(s2, s2);          //这个T推断出的类型是string ,因为形参本身就是const
14     
15     fun_c(s1, s2);          //错误,const不能自动转为非const
16     fun_c(s2, s2);          //这个T推断出的类型是const string
17     return 0;
18 }

  [3]函数模版也可以有用普通类型定义的参数,不使用T而使用int等等,这些参数可以进行正常的类型转换。意思就是说函数模板的参数可以是普通类型,例如template<typename T > int &my_vector(const T &a, int &b),正常的类型转换的意思就是  my_vector("s1",12),12可以正常转换为int类型。

  [4]尾置返回类型,这个意思就是返回的类型编译器推断不出,并且不用手工指定的一种方法,例如下面的例子,函数应该返回*begin类型,当然可以改下模板参数,手动指定一下返回类型,但是这里可以用decltype函数得到返回的类型。方法就是把返回类型设置为auto,然后用decltype获取到返回类型。正常的表达应该是template <typename It> decltype(*begin) fun(It begin, It end){},但是编译器在遇到参数列表之前,decltype(*begin)里面的begin是不存在的,所以就用的是下面这种格式。

 1 template <typename It> auto fun(It begin, It end) -> decltype(*begin)
 2 {
 3     return *begin;
 4 }
 5 
 6 int tmp;
 7 vector<int> v;
 8 
 9 v.push_back(1);
10 v.push_back(2);
11 
12 tmp = fun(v.begin(), v.end());

  [5]函数指针的处理,正常情况下,一个函数指针指向一个函数,这里一个函数指针可以指向一个模板的实例,这个模板实例化时的参数是根据函数指针确定的。

1 template <typename T> int fun(const T&, const T&);
2 int (*p)(const int &, const int &) = fun;       // p 指向模板的实例fun(const int &, const int &)
3 int (*p)(const int &, const int &) = fun<int>;  // 和上面的是一个意思

   [6]函数模板的重载,函数模版可以被另一个模版或者普通非模版函数重载,这个和普通重载是一样的。匹配规则:非模板函数优先于模板函数实例,如果没有非模板函数,则选择一个更加特例化的实例。

 1 void fun_a(int &v1){printf("call fun_a.\n");}
 2 template <typename T> void fun_a(const T &v1) {printf("call template fun_a(const T &v1).\n");};
 3 template <typename T> void fun_a(T *v1){printf("call template fun_a(T *v1).\n");};
 4 
 5 int main(){
 6     int a = 5;
 7     const int b = 5;
 8 
 9     fun_a(a);      //调用函数fun_a
10     fun_a(b);      //调用模板函数fun_a(const T &v1),因为b不是指针
11     fun_a(&b);     //调用模板函数fun_a(T *v1),因为这个更特例化,这个模板实例只能接受指针,而(const T &v1)可以接受除指针外的其他类型
12     return 0;
13 }

 5.模板的特例化

  [1]函数模板的特例化:有时候,模板函数处理传入的类型时,没办法进行处理,例如比较函数中传入字符串指针类型,这种情况就需要使用特例化,另外要清楚的是,特例化就是实现了此模板的一个实例,而非重载模板。特例化的方式就是template后面接空的<>,并且把参数列表的T替换为要实例化的类型。

 1 template <typename T> void fun_a(const T &v1) {printf("call template fun_a(const T &v1).\n");};
 2 
 3 //此处实例化了fun_a的一个实例,参数类型是const char *
 4 template <> void fun_a(const char * const &v1){printf("call template fun_a(const char * const &v1).\n");};
 5 
 6 int main()
 7 {
 8     const char *a = NULL;
 9     fun_a(a);
10     return 0;
11 }

  [2]类模板的特例化:这个就是实例化出一个特定的类型的类,例如下面的例子特例化出一个int类型参数的类,并且是部分特例化,这个和函数是类似的,template后面的尖括号保留不需要特例的化的参数,特例化的参数放到模板名后面。和函数不同的是,函数模板参数不能部分特例化。另外可以只特例化类中的成员函数。

#include <iostream>  
#include <stdio.h>  
#include <string.h>  
#include <vector>  
using namespace std;  

template <typename T, int size=100> class my_vector{
public:
    my_vector() {
        m_size = size;
        printf("call my_vector\n");
    }

    void fun(){printf("call my_vector fun()\n");};

private:
    int m_size;
    vector<T> m_data;
};

//特例化参数类型为string的成员函数
template<> void my_vector<string>::fun(){
    printf("call my_vector<string> fun()\n");
    return ;
}

// 特例化参数类型为int的实例
template<int size> class my_vector<int, size>{
public:
    my_vector() {
        m_size = size;
        printf("call my_vector<int, size>\n");
    }
    
    void fun(){printf("call my_vector<int, size> fun()\n");};

private:
    int m_size;
    vector<int> m_data;
};

int main(int argc, char *argv[])  
{  
    my_vector<int> int_vector1;

    printf("--------------------------\n");
    my_vector<string> str_vector1;
    str_vector1.fun();
    return 0;
}  

输出:

 6.trait

  当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同(而我们又不希望因为数据类型的差异而修改算法本身的封装时),traits会是一种很好的解决方案。这个意思就是说模板函数中需要根据不同的传入参数类型,进行不同的逻辑处理。例如一个计算总和的模板函数,需要根据传入的参数类型来设置总和的类型,例如传入的类型是int,则总和类型应该为long,传入的是char,则总和类型应该为int。这个可以通过增加模板参数来解决(例如增加一个参template <typename T,typename SUM_T>),但是这里可以用trait处理。

  具体做法就是增加一个基础模板,然后特例化这个基础模板,然后把这个基础模板应用到我们的函数模板中。另外特例化中的方法是为了初始化,所以使用statis会比较合适,当然不用statis也是可以的。当然还可以返回一个静态变量来完成初始化。这个没什么区别,只是为了初始化。

  这样做的好处就是函数模板还是一个,只是增加一些基础模板的特例化实例,参考下面的例子:

#include <iostream>  
#include <stdio.h>  
#include <string.h>  
#include <vector>  
using namespace std;  

//基础模板
template<typename T> class base_traits; 

//基础模板特例化int
template<> class base_traits<int>{ 
public: 
    typedef long sum_type; 
    static sum_type zero() { 
        printf("call base_traits<int> zero.\n");
        return 0; 
    } 

}; 

//基础模板特例化char
template<> class base_traits<char> { 
public: 
    typedef int sum_type; 
    static sum_type zero() { 
        printf("call base_traits<char> zero.\n");
        return 0; 
    } 

}; 

//计算总和的模板函数
template <typename T> inline typename base_traits<T>::sum_type sum (T const * beg, T const * end) 
{ 
    typedef typename base_traits<T>::sum_type sum_type; 

    sum_type total = base_traits<T>::zero();
    while (beg != end) { 
        total += *beg; 
        ++beg; 
    } 
    return total; 
} 

int main(int argc, char *argv[])  
{  
    int num[] = {1,2,3,4,5};
    printf("int sum:%d\n", sum(&num[0], &num[5]));
    
    char cnum[] = {11,12,13,14,15};
    printf("char sum:%d\n", sum(&cnum[0], &cnum[5]));

    return 1;
}  

  输出: