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


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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值