C++ Primer 第十六章 模板与范型编程

16.1 模板定义
    模板和c#范型一样,建立一个通用的类或函数,其参数类型和返回类型不具体指定,用一个虚拟的类型来代表,通过模板化函数或类实现代码在的重用。
    定义语法是:
    template<typename 类型参数> 
  返回类型 函数名(模板形参表) 
  {
    函数体 
  }
 
  或 :
  template<class 类型参数> 
  返回类型 函数名(模板形参表) 
  { 
    函数体
  }

    template是一个声明模板的关键字,类型参数一般用T这样的标识符来代表一个虚拟的类型,当使用函数模板时,会将类型参数具体化。typename和class关键字作用都是用来表示它们之后的参数是一个类型的参数。只不过class是早期C++版本中所使用的,后来为了不与类产生混淆,所以增加个关键字typename。
    函数模板:

template <typename T> //加法函数模板 
T Add(T x,T y) 
{
    return x+y; 
}; 
 
int main() 
{
    int x=10,y=10;
    std::cout<<Add(x,y)<<std::endl;//相当于调用函数int Add(int,int)
 
    double x1=10.10,y1=10.10;
    std::cout<<Add(x1,y1)<<std::endl;//相当于调用函数double Add(double,double)


 
    long x2=9999,y2=9999;
    std::cout<<Add(x2,y2)<<std::endl;//相当于调用函数long Add(long,long)

}

    template内可以定义多个类型形参,每个形参用,分割并且所有类型前面都要用typename修饰。
    template <typename T,typename Y> T Add(T x,Y y) ; // ok
    template <typename T,Y> T Add(T x,Y y) ; // 错误,Y之前缺少修饰符

    函数模板也可以声明inline 语法是 template <typename T,typename Y> inline T Add(T x,Y y) ;

    类模板:

template <typename T,typename Y>
class base
{
    public:
        base(T a);
        Y Get();

    private:
         T s1
         T s2 
};

int main() 

   base<int,string> it(1,"name"); //  类后面的类型参数不能缺省

}

    和函数模板不一样,类模板无法使用类型推断,所以定义对象时一定要显示传递类型参数。

    类型形参名称有自己的作用域:

typedef stirng T; // 该T与下面的类型形参不会产生冲突,不过最好不要重名以免混淆
template <typename T> 
 
T Add(T x,T y) 
{
    typedef stirng T; //
 错误,内部定义会产生名字冲突
    
//... 

};

    可以像申明一般函数或类一样声明(而不定义)。但类型形参不能省略 template <typename T,typename Y> class base ; 声明了一个类模板。

    模板类型参数可以用typename 或者class 来修饰,大部分情况下二者可以互换。但有一种特殊用方法时需要typename

class base
{
    public:
        class inbase{}; // 内部类

};

template <typename T> 
void test()
{
    typename T::inbase p; // 这时候必须要在前面加上typename,表示要定义一个类型为T类(T是类型参数)内部定义的inbase类对象

    T::inbase p; // 如果不加编译会报错,因为编译器认为T::inbase表示T类的静态成员inbase,所以这样书写语法是错误的
}

    要注意,这种用法需要满足条件:类型形参T必须要定义内部类inbase 否则会编译错误。

    模板编程中还可以在类型形参列表中定义非类型形参,这时非类型形参会被当成常量

template <typename T,int i> 
 
T Add(T x) // Add(T x,int i) 这样定义编译错误,i 和非形参i名称冲突

{
    return x + i;
};
  
int main() 

   Add<int,10>(5);
}

 

    范型编程有两个重要原则:形参尽量使用const引用(防止拷贝),形参本身操作尽量少(传递一个不支持函数形参体操作的类型会报错)


16.2 实例化
    函数模板可以定义函数指针并予以赋值

template <typename T,typename Y> T Get(T x,Y y) ; // 声明函数
int(*pr) (int,string) = Get ; // 定义函数指针并赋值
pr(5,"str") ;  // 用函数指针调用函数无需解引,或者(*pr)(5,"str") ;

函数模板指针作为形参时需注意重载情况。对二义性的调用要指定类型来消除

template <typename T> T Get(T x) ; // 声明函数
void fun(int (*) (int));
void fun(string (*) (string));

fun(Get); // 错误,有二义性,类型推断后重载的两个fun函数都能通过。

fun(Get<int>); // 指定类型,消除了二义性


16.3 模板编译模型

    [1] 当编译器看到模板定义的时候,它不立即产生代码。 只有在看到用到模板时 ,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例 。
    [2] 一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
    [3] 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。 当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要哪些通常放在源文件中的代码。
    [4] 标准C++为编译模板代码定义了两种模型。 所有编译器都支持第一种模型,称为“包含”模型( inclusion compilation model) ;只有一些编译器支持第二种模型,“分别编译”模型( separate compilation model) 。
    [5] 在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义 。
    [6] 在包含编译模型,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#include引入了包含相关定义的源文件 。
    [7] 在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事 。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化 。
    [8] 在一个程序中,一个模板只能定义为导出一次。 一般我们在函数模板的定义中指明函数模板为导出的 ,这是通过在关键字template之前包含export关键字而实现的。对类模板使用export更复杂一些 ,记得应该在类的实现文件中使用export,否者如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。
    [9] 导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export不在类模板本身指定,而是只在被导出的特定成员定义上指定。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。

16.4 类模板成员
    普通类不但定义非模板函数成员,也能定义模板函数成员:

class base
{
    public:
        template<typename T> T Get(T a); // 模板函数成员申明

};

template<typename T> T base::Get(T a) //成员函数类外部定义

{
    return a;
}

    可这样调用:
    base obj ;
    obj.Get<int>(20) ;
    obj.Get("str") ;  // 类型推断,等价于obj.Get<string>("str") ;


    如果是模板类

template<typename T>
class base
{
    public:
        template<typename Y> Y Get(Y a); // 模板函数成员申明

};

template<typename T> // 这一步不可少,确定T也是个模板类型参数

template<typename Y> Y base<T>::Get(Y a)
{
    return a;
}

    可这样调用:
    base<string> obj ;
    obj.Get<int>(20) ;
    obj.Get("str") ; // 类型推断,等价于obj.Get<string>("str") ;

    类模板或函数模板可以作为其他类的友元,不过由于其特殊性可以做一些限制。

template<typename T>
class he
{
    // ...

}

template<typename T>
class base
{
    template<typename Y> friend class he; // 表示所有类型的模板类对象都是友元

    friend class he<int>; // 表示只有int类型形参的模板类对象才是友元 
    friend class he<T>;   // 表示只有类型形参和base类型参数一致的模板类对象才是友元
}

    友元函数和模板类情况相似。 第一种友元可以看做是完全申明,第二种和第三种友元则需要至少在base定以前有完全申明,否则会编译错误。

16.5 一个范型句柄类
    如果对上一章句柄类有充分理解范型句柄类应该非常容易掌握。

16.6 模板特化
    模板的特化(template specialization)分为两类:函数模板的特化和类模板的特化。
    函数模板的特化:当函数模板需要对某些类型进行特别处理,称为函数模板的特化。例如:

bool IsEqual(T t1, T t2) 
{
     return t1 == t2; 
};

int main()
{
     char str1[] = "Hello";

     char str2[] = "Hello";

     cout << IsEqual(11) << endl;

     cout << IsEqual(str1, str2) << endl;   //输出0

 return 0;
 
}

    最后一行比较字符串是否相等。由于对于传入的参数是char *类型的,IsEqual函数模板只是简单的比较了传入参数的值,即两个指针是否相等,因此这里打印0。显然,这与我们的初衷不符。因此,sEqual函数模板需要对char *类型进行特别处理,即特化:

template <> bool IsEqual(char* t1, char* t2) // 函数模板特化
{
    return strcmp(t1, t2) == 0;
}


    这样,当IsEqual函数的参数类型为char* 时,就会调用IsEqual特化的版本,而不会再由函数模板实例化。

    类模板的特化:与函数模板类似,当类模板内需要对某些类型进行特别处理时,使用类模板的特化。例如:

template <class T>
class compare 
{
  public:
    bool IsEqual(T t1, T t2)
    {
       return t1 == t2;
    }
};   
 
int main() 
{
  char str1[] = "Hello";

  char str2[] = "Hello";

  compare<int> c1;

  compare<char *> c2;
  cout << c1.IsEqual(11) << endl; //比较两个int类型的参数


  cout << c2.IsEqual(str1, str2) << endl;   //比较两个char *类型的参数
  return 0
}

    这里最后一行也是调用模板类compare<char*>的IsEqual进行两个字符串比较,显然这里存在的问题和上面函数模板中的一样,我们需要比较两个字符串的内容,而

不是仅仅比较两个字符指针。因此,需要使用类模板的特化:

template<>class compare<char *> //特化(char*) 
{
  public:
     bool IsEqual(char* t1, char* t2)
     { 
        return strcmp(t1, t2) == 0;  //使用strcmp比较字符串

     }

};

    注意:进行类模板的特化时,需要特化所有的成员变量及成员函数。

原文地址:https://www.cnblogs.com/kingcat/p/2514939.html