C++模板:究竟什么是特化?

已经制定了一个计划,希望可以定期地在博客上更新文章。我将探讨编译器如何处理模板等这类有意思的事情,并佐以实例。


作为一个编译器开发人员,我倾向于用一些小的例子来显示或者测试编译器是如何工作的,而不是来指导你如何在一个应用程序中使用编译器的某个功能。
或许会有些人觉得这是有趣的事情。关于这个话题,我有很多想法,也希望大家能多提供建议。


我觉得比较有意思的一件事是编译器如何来处理重载解析,更具体的讲,是偏特化模板
如何允许你利用重载解析来定义一系列的类。下面我会用一系列的例子来解释这个问题。
struct t1{}; struct t2{}; struct t3{};

void func(t1 arg){ printf("called t1\n"); }
void func(t2 arg){ printf("called t2\n"); }
void func(t3 arg){ printf("called t3\n"); }

int main(void)
{
t1 x1; t2 x2; t3 x3;
func(x1);
func(x2);
func(x3);
return 0;
}
运行结果:
called t1
called t2
called t3



这个很简单,编译器根据传递给函数的实参类型来决定调用哪个函数,这就是重载解析。在调用前,编译器有一个候选函数调用列表:
  1. void func(t1);
  2. void func(t2);
  3. void func(t3);
每个调用函数都有各自的参数,编译器根据参数最匹配原则选择相应的函数 

现在来看一个模板函数:
#include <iostream>
#include <typeinfo>

struct t1{}; struct t2{}; struct t3{};

using namespace std;

template <class A, class B, class C> void func(A a1, B a2, C a3)
{
   cout << "A: " << typeid(a1).name() << endl;
   cout << "B: " << typeid(a2).name() << endl;
   cout << "C: " << typeid(a3).name() << endl;
}

int main(void)
{
  t1 x1; t2 x2; t3 x3;
  func(x1,x2,x3);
  return 0;
}
运行结果:
A: t1
B: t2
C: t3

在这个使用了一个函数模板的例子中,编译器有一个带有3个未知类型<A,B,C>的候选调用函数,它将实参 (x1,x2,x3)传递给函数func中的3个形参(A,B,C),可以很容易看到编译器是如何推导出模板参数的:
A t1
B t2
C t3

编译器实例化了模板函数:将实参传递给模板函数中的形参以创建一个真正的函数:
void func(t1 a1, t2 a2, t3 a3)

如果有其他的候选重载函数,他们都将会和非模板函数的例子一样被绑定在一起,然后在重载解析中根据实参类型调用相应的函数。
重载解析 允许用户 创建同一 个函数的 不同版本 ,这些函 数将根据 传进来的 参数的类 型,做一 些不同的 操作。编 译器会根 据类型信 息来选择 相应的函 数。通过 使用模板 函数,用 户可以定 义带参数 化类型的 函数,从 而减少需 要定义的 重载函数 的个数。 编译器会 选择正确 的模板并 为用户创 建候选的 重载函数

现在来看一个类模板:
#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

int main(void)
{
   container<t1,10> test;
   test.callMe();
   return 0;
}
运行结果:
primary A: t1 I: 10


在这个例子中,编译器并不会玩什么把戏,这个例子中只有一个类container, 它接收了实参<t1,10>并传递给模板参数<A, I>,推导出A即为t1,I为10。
再看: 

#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99" << endl;
   }
};

int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   return 0;
}
运行结果
primary A: t1 I: 10
complete specialization t3, 99
 

在这个例子中有两个模板,其中一个是全特化模板,即模板中模板参数全部指定为确定的类型。特化(specialized)不过是一个花哨的术语,意思是形参不再为形参,它们已经有了确定的值。我更倾向于使用“全特化”这个术语,感觉这更容易让人理解。但是在大多数的C++书籍,包括标准C++,都将其称为“显示特化”。
现在编译器有了两个类名都为container的类模板,类模板被重载:
  1. template <class A, int I> struct container;
  2. template <> struct container<t3,99>;
当编译器执行到container<t1,10>test1, 对于参数<t1, 10>:
- 候选模板1可推出 <A=t1, I=10> ,所以候选模板1有效; 
- 候选模板2无法推出<t3,99> 能与 <t1,10>匹配,所以候选模板2被剔除。
这样编译器只有一个候选模板1,也即最终被匹配的模板。
当编译器执行到container<t3, 99>test2,对于参数<t3, 99>:
- 候选模板1可推出<A=t3, I=99>,所以候选模板1有效
- 候选模板2,很明显 <t3,99> 与模板中的 <t3,99>相匹配,所以候选模板2有效。
当在一个程序中发现有两个或者两个以上候选模板有效时,编译器根据最匹配原则选择最为匹配的那个模板,即候选模板2。

下一个例子:
#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <class A1>  struct container<A1,25>{
   void callMe(){
      cout << "partial specialization" << typeid(A1).name() << " and 25 " << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99" << endl;
   }
};


int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   container<t2,25> test3;
   test3.callMe();
   container<t3,25> test4;
   test4.callMe();
   return 0;
}
运行结果:
primary A: t1 I: 10
complete specialization t3, 99
partial specializationt2 and 25 
partial specializationt3 and 25 

在这个例子中有3个候选模板:
  1. template <class A, int I> struct container;
  2. template <class A1> struct container<A1,25>;
  3. template <> struct container<t3,99>;
 
模板1是带有两个模板参数的主模板,模板2是带有一个模板参数的偏特化模板,模板3是无模板参数的全特化模板。
如前面所说,偏特化也仅是一个花哨的术语,偏特化模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。
当编译器编译执行到container<t3,25> test4,参数为<t3,25>:
- 候选模板1,编译器可推导出 <A=t3, I=25>,故候选模板1有效;
- 候选模板2,编译器为偏特化模板可推导出
<A1=t3, 25>,故候选模板2有效;
- 候选模板3, 编译器不可能从
<t3,25>得到<t3,99>,故候选模板3被剔除。
候选模板2是最匹配的模板,故匹配模板2。

下面的例子有一些微小的变化:
#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <int I1>  struct container<t3,I1>{
   void callMe(){
      cout << "partial specialization t3 and " << I1  << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99 " << endl;
   }
};


int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   container<t3,75> test3;
   test3.callMe();
   container<t3,25> test4;
   test4.callMe();
   return 0;
}
运行结果:
primary A: t1 I: 10
complete specialization t3, 99 
partial specialization t3 and 75
partial specialization t3 and 25


 
本质上,偏特化模板的匹配和选择过程与重载解析非常类似。实际上,在非常复杂的偏特化情况下,编译器可能就是将偏特化直接译成函数,然后直接调用重载解析来处理。

重载解析和偏特化匹配都用到了模板参数推导。

原文地址:https://www.ibm.com/developerworks/mydeveloperworks/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/c_templates_what_s_all_this_specialization_about_anyway8?lang=en


最近看了<The C++ Programing Language>看到了模板的特化,突然想起来<C++ Primer>上说的显式具体化、隐式具体化、特化、偏特化、具体化等概念弄得头晕脑胀,我在网上了找了好多帖子,才把概念给理清楚。

      一下是我把再网上找的资料整理一下。

      看着这么多叫法,其实就是三种。

     1. 显示实例化

     2. 隐式实例化

     3. 特化(=具体化)、偏特化


一、实例化

1.显示、隐式实例化

      什么是实例化:一个通过使用具体值替换模板参数,从模板产生的普通类,函数或者成员函数的过程。

     显示实例化:通过名字可见,就是清楚的表明你要实例化的类型

     隐式实例化:通过编译器自己推测判断要实例化的类型。

    比如一个模板:

[cpp]  view plain  copy
 print ?
  1. template<class T> //函数模板实现  
  2. void swap(T &a, T &b)  
  3. {  
  4.     T temp;  
  5.     temp = a;  
  6.     a = b;  
  7.     b = temp;  
  8. }  

 a. 显示实例化

     template  void swap<int>();  // 无须给该函数重新编写函数体,这只是个声明

     为什么要显示实例化? 

     主要是提高效率,当显式实例化模板时,在使用模板之前,编译器根据显式实例化指定的类型生成模板实例,这样就相当于本程序里面有个一

[cpp]  view plain  copy
 print ?
  1. void swap(int &a, int &b)  
  2. {  
  3.    int temp;  
  4.    temp = a;  
  5.    a = b;  
  6.    b = temp;  
  7. }  
这样的话,每次需要调用 swap<int>(a,b)的时候每次都重新生成该类型的代码,可以节省空间,也能提高效率。这就是为什么要是显式的实例化的原因。

b. 隐式实例化

    隐式实例化指的是:在使用模板之前,编译器不生成模板的声明和定义实例。只有当使用模板时,编译器才根据模板定义生成相应类型的实例。

   int i=0, j=1;
   swap(i, j); //编译器根据参数i,j的类型隐式地生成swap<int>(int &a, int &b)的函数定义。

   隐式实例化就是程序员为了省事,把类型省略让编译器判断,这是一个偷懒的表现吧。


二、特化

1.  特化(=具体化)

     然而通常又有一些特殊的情况,不能直接使用泛型模板展开实现,这时就需要针对某个特殊的类型或者是某一类特殊的类型,而实现一个特例模板————即模板特化

    当T如果为 一个 struct类型的,它的交换就无法进行,所以我们针对这种特殊的情形,我们专门写了一个函数,只有当T为 这种struct类型时候,才会调用这个特化的函数

[cpp]  view plain  copy
 print ?
  1. //对函数  
  2. #define MAXNAME 128  
  3. struct job  
  4. {  
  5. char name[MAXNAME]:  
  6. int salary;  
  7. };  
  8.   
  9. template<class T>  
  10. void swap(T &a, T &b )  
  11. {  
  12.   T temp;  
  13.   temp = a;  
  14.   a = b;  
  15.   b = temp;  
  16. };  
  17.   
  18. template void swap<int>(int &a, int & b);  //显式实例化,只需声明  
  19.   
  20. template<> void swap<job>(job &a, job &b)   //显式具体化(上面已经讲过,注意与实例化区分开,必须有定义)  
  21. {  
  22.   int salary:  
  23.   salary = a.salary:  
  24.   a.salary = b.salary;  
  25.   b.salary = salary;  
  26. };//explicite specialization.  
  27.   
  28. //对类模板:  
  29. template <class T>  
  30. class Arrary  
  31. {  
  32. private:  
  33.   T* ar;  
  34.   int l;  
  35. ...  
  36. };//template class declaration.  
  37.   
  38. template class Array<int>;   //explicit instantiation. 显式实例化  
  39.   
  40. template<> class Array<job>  
  41. {  
  42. private:  
  43.   job* ar;  
  44.   int l;  
  45. };//expicit specialization.   显式具体化,类定义体可以不同于类模板Array  


2. 偏特化

    模板的偏特化是指需要根据模板的部分参数进行特化。

a. 类模板的偏特化

例如c++标准库中的类vector的定义

[cpp]  view plain  copy
 print ?
  1. template <class T, class Allocator>  
  2. class vector { // … // };  
  3. template <class Allocator>  
  4. class vector<bool, Allocator> { //…//};  
  5. //这个偏特化的例子中,一个参数被绑定到bool类型,而另一个参数仍需要由用户使用时指定。  
b. 函数模板的偏特化

网上看到有人说:从严格意义上讲,函数模板并不支持偏特化(我对这个不是很理解),但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。
比如:
a) template <class T> void f(T); 
    根据重载规则,对a)进行重载
b) template < class T> void f(T*); 
    如果将a)称为基模板,那么b)称为对基模板a)的重载,而非对a)的偏特化。
这里我就不深入的剖析偏特化了。

三、模板的匹配顺序

1. 类模板的匹配规则

例如:
template <class T> class vector{//…//}; // (a) 普通型
template class vector<typename> ; // (b) 的显式实例化
template <class T> class vector<T*>{//…//}; // (c) 对指针类型特化
template <> class vector <void*>{//…//}; // (d) 对void*进行特化
每个类型都可以用作普通型(a)的参数,但只有指针类型才能用作(b)的参数,而只有void*才能作为(c)的参数

所以,当一个调用一个模板类,首先,找显式实例化的,如果不匹配;接着,找特化的,然后,找偏特化的,最后,根据模板隐式实例化

2.函数模板的匹配规则

例如:

void swap(int &a, int &b){} // 普通的函数
template<> swap<int>(int &a, int &b){} // 特化的模板函数
template void swap<int>(int &a, int &b); // 显式实例化,这个只用声明就行
template<class T> void swap(T &a, T &b){} // 模板

以上书写的顺序就是模板的调用顺序。

原文地址:http://blog.csdn.net/chenyiming_1990/article/details/10526371


以上两篇结合看,基本上可以把握概念和使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值