资讯中心
C++ 模板元编程之模板特化的见解从何而来
发布日期:2022-08-07 00:28    点击次数:173
  0. 前言

C++ 里的模板能做什么呢?它比如 C 言语中的宏、C# 和 Java 中的自省(restropection)和反射(reflection),是 C++ 言语的外延。更极端一点天文解:它是一门新的图灵一切的编程言语(也就是说,C++ 模板能实现图灵机模型里的整个功用)。在《Modern C++ Design》中,作者抛出了下列几个成就:

(1)怎么撰写更低档的C++程式?

(2)怎么应付纵然在很洁净的策画中仍然像雪崩同样的不相关细节?

(3)怎么构建可复用组件,使得每次在差别程式中应用组件时无需大动兵戈?

经管上述成就的编制就是模板元编程。元(meta)本身就是个很“笼统(abstract)”的词,因为它的本意就是“笼统”。元编程,也可以说就是“编程的笼统”。用更好理解的说法,元编程意味着你撰写一段顺序A,顺序A会运行西席成此外一个顺序B,顺序B才是真正实现功用的顺序。那末这个岁月顺序A可以或许称作顺序B的元顺序,撰写顺序A的进程,就称之为“元编程”。

C++中,元编程的伎俩,可以或许是宏,也可所以模板。

一、为何必要泛型编程:从宏到模板,再到元编程

假定元编程中全体的变量(或许说元编程的参数),都是范例,那末这样的编程,我们有个特定的称说,叫“泛型”。

模板的发明,仅仅是为了做和宏险些同样的替代事变吗?可以或许说是,也可以说不是。

一方面,模板可以或许用来替代范例,这点和宏没什么差别。只是宏在编译阶段基于文本做纯正替代,被替代的文本本身没有任何语义。而模板会在阐发模板时以及实例化模板的岁月都市举行查抄,而且源代码中也能与调试标志逐个对应,所以不管是编译时照旧运行时,排错都相对俭朴。

另外一方面,模板和宏也有很大的差别,模板最大的差别在于它是“可以或许运算”的。我们来看一个例子:

void Add(uint8_t, unit8_t) {} 

上述函数实现了一个 uint8_t 和 uint8_t 范例的加法运算,假定今朝要实现 int16 和 int16 范例的加法运算,该怎么办呢?俭朴点的编制以下:

if (type == 8) {     Add(uint8_t, uint8_t) } else if (type == 16) {     Add(uint16_t, uint16_t) } 

然则这里有两个难点:

首先, if(type == x) 是不存在于 C++ 中的 其次,即便存在取得变量 type 的编制(Boost.Any 中的 typeid ),我们也不停留它在运行时鉴定,这样会变得很慢。是否可以或许不引入 if/else,在编译期就把 Add 的编制肯定呢?

有人说,重载、虚函数也能经管如上成就:

void Add(uint8_t, uint8_t) {} void Add(uint16_t, uint16_t) {} 

以至在 C 言语中定义新的组织体 Variant 或应用 void* 也能经管该成就:

struct Variant {     union     {         uint_8 x;         uint_16 y;     } data;     uint32_t typeId; }; 

没错,然则假定我另有 uint9_t、uint10_t 等各类范例的加法运算呢?Anyway,不论是哪种编制都很难防止 if/else 的存在。

模板与上述这些编制最大的差别在于:模板不管其参数或许是范例,它都是一个编译期分拨的编制。编译期便可以或许肯定的货物既可以或许做范例查抄,编译器也能举行优化,砍掉任何毋庸要的代码执行门路。

二、类模板的特化:模板世界里的 if/else 2.1 痛处范例执行代码

我们先来看看一个模板的例子:

template <typename T> T AddFloatOrMulInt(T a, T b); // 我们停留这个函数在 T 是 float 的岁月做加法运算,在 T 是 int 范例的岁月做乘法运算 

那末当传入两个差别范例的变量,或许不是 int 和 float 变量,编译器就会提示舛误。

从才能下去看,模板能做的事变都是编译期实现的。编译期实现的意义就是,当你编译一个顺序的岁月,全体的量就都已经肯定了。比喻下面的例子:

int a = 3, b = 5; Variant aVar, bVar; aVar.setInt(a);            // 我们新加之的编制,怎么实现的无所谓,资讯中心巨匠显然意义就好了。 bVar.setInt(b); Variant result = AddFloatOrMulInt(aVar, bVar); 

从上述代码中我们可以或许看到:aVar 和 bVar 都必定会是整数。所以假定有相宜的机制,编译器便可以或许晓得此处的 AddFloatOrMulInt 中只有要执行 int 门路上的代码,而且编译器在此处也能零丁为 int 门路生成代码,从而去掉那个毋庸要的 if。在模板代码中,这个“相宜的机制”就是指“特化”和“部份特化(Partial Specialization)”,后者也叫“偏特化”。

2.二、怎么写模板特化的代码

1.0 版本 - 伪代码

int/float AddFloatOrMulInt(a, b) // 类的动静函数 {   if(type is int) {     return a * b;   } else if (type is float) {     return a + b;   } }  void foo() {     float a, b, c;     c = addFloatOrMulInt(a, b);        // c = a + b;      int x, y, z;     z = addFloatOrMulInt(x, y);        // z = x * y; } 

2.0 版本 - 函数重载

float AddFloatOrMulInt(float a, float b) {     return a + b; }  int AddFloatOrMulIntDo(int a, int b) {     return a * b; }  void foo() {     float a, b, c;     c = AddFloatOrMulInt(a, b);        // c = a + b;      int x, y, z;     z = AddFloatOrMulInt(x, y);        // z = x * y; } 

3.0 版本 - 纯模板

// 这个是给float用的。 template <typename T> class AddFloatOrMulInt {     T Do(T a, T b)     {         return a + b;     } };  // 这个是给int用的。 template <typename T> class AddFloatOrMulInt {     T Do(T a, T b)     {         return a * b;     } };  void foo() {     float a, b, c;      // 我们必要 c = a + b;     c = AddFloatOrMulInt<float>::Do(a, b);             // ... 感应何处纰谬劲 ...     // 啊!有两个 AddFloatOrMulInt,class 看起来一模一样,要怎么判别呢! } 

好吧,成就来了!怎么要让两个内容差别,然则模板参数模式沟通的类举行判别呢?特化!特化(specialization)是痛处一个或多个不凡的整数或范例,给出模板实例化时的一个指定内容。

4.0 版本 - 模板特化

// 首先,要写出模板的普通模式(原型,即初始化,不克不迭省) template <typename T> class AddFloatOrMulInt {     static T Do(T a, T b)  // 留心这里必须得是动静编制!!!     {         return T(0);     } };  // 其次,我们要指定T是float岁月的代码: template <> class AddFloatOrMulInt<float> { public:     static float Do(float a, float b)     {         return a + b;     } };  // 再次,我们要指定T是int岁月的代码,这就是特化: template <> class AddFloatOrMulInt<int> { public:     static int Do(int a, int b) //      {         return a * b;     } };  int foo() {     return AddFloatOrMulInt<float>::Do(1.0, 2.0); }  int main()  {     std::cout << foo();  // 输出终局 3.0 } 

说明:

// 我们这个模板的根抵模式是什么? template <typename T> class AddFloatOrMulInt;  // 然则这个类,是给T是 int 的岁月用的,是以我们写作 class AddFloatOrMulInt<int>; // 固然,这里编译是通不过的。 // 然则它又不是个通俗类,而是类模板的一个特化(特例)。 // 所从前面要加模板关键字template,以及模板参数列表 template </* 这里要填什么? */> class AddFloatOrMulInt<int>;  // 最后,模板参数列表内里填什么?因为原型的T已经被int庖代了。所以这里就不克不迭也不必要放任何额外的参数了。所以这里放空。 template <> class AddFloatOrMulInt<int> {     // ... 针对 int 的实现 ...  }  // Done! 

至此,第一个模板特化的代码已经写完了。这里的 AddFloatOrMulInt 似乎是一个函数,却只能在编译时期执行。假定你体会到了这一点,那末祝贺你,你的模板元编程已经开悟了。

三、总结

 

本文焦点只讲了两个成就:一是为何必要泛型编程,重点介绍了宏、模板和元编程的纠葛;二是模板类的特化代码怎么编写。对付特化,另有良多细节知识,在当前的文章中我们延续探讨,此外将还介绍偏特化等知识点,敬请等候。