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

关键词:特化,全特化,偏特化;模板参数推导;函数重载


中文链接:

https://www.ibm.com/developerworks/mydeveloperworks/blogs/12bb75c9-dfec-42f5-8b55-b669cc56ad76/entry/c__e6_a8_a1_e6_9d_bf__e7_a9_b6_e7_ab_9f_e4_bb_80_e4_b9_88_e6_98_af_e7_89_b9_e5_8c_96?lang=zh&cmp=dw&cpb=dw&ct=dwcom&cr=dwcom&ccy=cn


英文链接:

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



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

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


我觉得比较有意思的一件事是编译器如何来处理重载解析,更具体的讲,是偏特化模板
如何允许你利用重载解析来定义一系列的类。下面我会用一系列的例子来解释这个问题。
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


在这个例 子中,编 译器并不 会玩什么 把戏,这 个例子中 只有一个 类con tain er, 它接收了实参< 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


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






你可能感兴趣的:(C++模板:究竟什么是特化?)