C++模板:什么是特化?学习笔记

谁都会走 提交于 2019-12-15 06:20:04

参考:IBM编译器中国开发团队博客
其中几个比较好的例子,下面的class 都可以换成 typename,向后兼容性比较好。
而且typename是较class更加新的标准,具体class 可能导致的问题可见这篇文章,讲的特别详细
知无涯值C++ typename

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

这个很简单,编译器根据传递给函数的实参类型来决定调用哪个函数,这就是重载解析。在调用前,编译器有一个候选函数调用列表:
void func(t1);
void func(t2);
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的类模板,类模板被重载:
template <class A, int I> struct container;
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个候选模板:
template <class A, int I> struct container;
template struct container<A1,25>;
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

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!