【C++初阶:模板进阶】非类型模板参数 | 模板的特化 | 模板分离编译 下

简介: 【C++初阶:模板进阶】非类型模板参数 | 模板的特化 | 模板分离编译

💦 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板。
  2. 关键字 template 后面接一对空的尖括号 <>。
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
  4. 函数形参表必须要和函数模板的基础参数类型完全相同,如果是不同编译器可能会报一些奇怪的错误。
template<class T>
bool IsEqual(const T& left, const T& right)
{
  return left == right;
}
//函数模板匹配原则
//err,表达式必须是可修改的左值,p1 and p2做为形参传给left and right,并且是p1 and p2的别名,这里 p1 and p2 是数组名,带有const属性,注意实参的const修饰的是*left,这里属于权限放大。
//bool IsEqual(const char*& left, const char*& right)
/*bool IsEqual(const char* const& left, const char* const& right)//ok,这里就非常考验咱基础扎实与否了,const在*左边,修饰*left,const在*右边,修饰left。
{
  return strcmp(left, right) == 0;
}*/
//同上,不使用引用就可以不用const,因为这时是值拷贝,并不会影响实参。
/*bool IsEqual(const char* left, const char* right)
{
  return strcmp(left, right) == 0;
}*/
//函数模板的特化,有bug,待改
template<>
bool IsEqual<const char*>(const char* const& left, const char* const& right)
{
  return strcmp(left, right) == 0;
}
int main()
{
  cout << IsEqual(1, 2) << endl;//ok
  char p1[] = "hello";
  char p2[] = "hello";
  cout << IsEqual(p1, p2) << endl;
  return 0; 
}

📝说明

严格的说,以上 2 种写法不是特化,而是模板的匹配原则 —— a) 有现成完全匹配的,就直接调用,没有现成调用的,实例化模板生成。 b) 有需要转换匹配的,那么它会优先选择去实例化模板生成。

再来看一个例子:

template<class T>
void Swap(T& a, T& b)
{
  //对于v1 and v2对象虽然Swap能成功,但是Swap里会完成3次深拷贝,所以针对v1 and v2我们有必要做特殊处理。
  T tmp = a;
  a = b;
  b = tmp;
}
//模板匹配原则来进行特殊处理
/*void Swap(vector<int>& a, vector<int>& b)
{
  a.swap(b);
}*/
//函数模板的特化,标准的特殊化处理
template<>
void Swap<vector<int>>(vector<int>& a, vector<int>& b)
{
  a.swap(b);
}
//对于下面的v3 and v4,目前好像只能这样特化
template<>
void Swap<vector<double>>(vector<double>& a, vector<double>& b)
{
  a.swap(b);
}
int main()
{
  int x = 1, y = 2;
  Swap(x, y);
  vector<int> v1 = { 1, 2, 3, 4 };
  vector<int> v2 = { 10, 20, 30 };
  Swap(v1, v2);
  vector<double> v3 = { 1.1, 2.2, 3.3, 4.4 };
  vector<double> v4 = { 10.1, 20.2, 30.3 };
  Swap(v3, v4);
  return 0;
}

📝说明

对于模板匹配原则 and 函数模板特化,两者底层并无差别,如果能使用模板匹配原则特化就更推荐使用模板匹配原则来进行特化。

💦 类模板特化

1、全特化

全特化:即是将模板参数列表中所有的参数确定化。

template<class T1, class T2>
class Data
{
public:
  Data(){cout << "Data<T1, T2>" << endl;}
private:
  T1 _d1;
  T2 _d2;
};
//全特化
template<>
class Data<double, double>
{
public:
  Data(){cout << "Data<double, double>" << endl;}
private:
};
int main()
{
  Data<int, int> d1;
  Data<double, double> d2;
  return 0;
}
2、偏特化

偏特化 (半特化):任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

template<class T1, class T2>
class Data
{
public:
  Data(){cout << "Data<T1, T2>" << endl;}
private:
  T1 _d1;
  T2 _d2;
};
//偏特化(半特化)
//只要第二个模板参数是char,那么它就会匹配
template<class T1>
class Data<T1, char>
{
public:
  Data(){cout << "Data<T1, char>" << endl;}
private:
  T1 _d1;
};
//当两个模板参数是指针就会匹配,不管是什么类型的指针
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
  Data(){cout << "Data<T1*, T2*>" << endl;}
private:
  T1 _d1;
  T2 _d2;
};
//T1&, T2&
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
  Data(){cout << "Data<T1&, T2&>" << endl;}
private:
  T1 _d1;
  T2 _d2;
};
//T1&, T2*
template<class T1, class T2>
class Data<T1&, T2*>
{
public:
  Data(){cout << "Data<T1&, T2*>" << endl;}
private:
  T1 _d1;
  T2 _d2;
};
int main()
{
  Data<double, int> d1;
  Data<double, char> d2;
  Data<int*, char*> d3;
  Data<int&, char&> d4;
  Data<int&, char*> d5;
  return 0;
}

📝说明

偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,比如说限定你的类型是指针。

在前面谈到的类型萃取本质就是特化,关于特化的场景我们现在还不好举例,等后面的哈希表会再见面。

三、模板分离编译

💦 什么是分离编译

一个程序 (项目) 由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

💦 模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

背景 ❗

在 C语言实现数据结构时,我们写的顺序表、链表等,都是写一个 SeqList.h 文件用于声明,SeqList.c 用于定义,test.c 用于测试。而到 STL 这里都是定义 vector.h 用于声明定义或定义,test.cpp 用于测试。这是因为 C++ 里的模板不支持分离编译。

可以看到这里调用 F 后报了链接错误,链接错误一般都是在链接时找不到它的定义,但是我这里有定义 F 的呀,相比 Print 都找的到,而 F 为啥找不到 ❓

我们先回顾下程序的编译过程 Func.h | Func.cpp | Test.cpp:

  1. 预处理 —— 头文件展开、宏替换、条件编译、去掉注释后,生成一份干净的 C 原始程序。
    Func.i | Test.i

  2. 编译 —— 语法检查后,生成汇编代码。
    Func.s | Test.s

  3. 汇编 —— 把汇编代码转成二进制机器码
    Func.o | Test.o
  4. 链接 —— 把类似 Test.o 里面 F and Print 这样没有地址的地方,拿修饰过的函数名去 Func.o (符号表里会把函数名和对应地址建立起来) 中查找,找到后填上地址。再把目标文件合并到一起,生成可执行程序。

为什么不能分离编译 ❓

用函数名去查找时 Print 能找到,但是 F 找不到,如下标识处就是 Windows 下函数名 F 的修饰规则修饰出来的函数名。

因为在链接之前,这 2 个文件都是各自玩各自的,只有在链接时才会交汇。Func.i 编译成 Func.s 时就存在一个问题,Print 有定义可以生成,但是 F 是 1 个模板,它不能生成,因为不知道 T 是什么类型,这里只有 Test.i 才知道 T 是什么类型,等到链接时就晚了,所以它找不到 F 的定义。

💦 解决方法

☣ A):

先说一下不可行的方法,让编译器在编译的时候去各个地方查找实例化,比如说在 Func.i 里看到有 1 个模板,然后去 Test.i 里找实例化,但是这样对于编译器的实现就复杂了,这样说的原因是如果是大项目,有几百个文件的情况下,那么成本就非常高了。所以说在链接之前,它们是不会互相交互的。

☣ B):

显示指定实例化,编译器看到后会就知道你要把这个 T 实例化什么类型。

但是显示实例化带来的问题是我换个类型就又链接不上了,因为你这里只显示实例化了 int,解决方法是再显示实例化对应类型,这种方式的缺陷也很明显 —— 使用一种类型就得显示实例化一个,很麻烦,一点也不通用。

☣ C) 推荐:

这种方法非常的粗暴,STL 源码中也是使用这种粗暴的方案,不分离编译,声明和定义或者直接定义在 Func.h 中。因为 Func.h 中包含了模板的定义,就不需要链接的时候去查找了,直接在编译阶段就直接填地址了。有些地方可能会把就种声明和定义放一起的模板,它会定义成 Func.hpp,也就是说它既是 .h,也是 .cpp。

分离编译扩展阅读

💦 补充

同样我们的类模板也不支持分离编译,最好的办法就是不分离,要调用构造、析构,需要找它们的地址,此时就不需要在链接时去找了,在编译时既有声明也有定义,然后这里编译成指令的同时就可以拿到它们的地址了。

按需实例化 ❓

紧接着,我们又实现了一个 push,并且 push 的定义里有一个语法错误 —— 少一个分号,但是奇怪的是我竟然能编译通过。原因其实也很简单,模板如果没有实例化,编译器不会去检查模板函数内部的语法错误。

四、模板总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++ 的标准模板库 (STL) 也因此而产生。
  2. 增强了代码的灵活性

【缺点】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,且准确度不高 (不要轻易去相信模板的报错),不易定位错误。可能只是一个小错误,却报出一大串的错误 (深有体会),此时一定要优先看第一个错误。

但是整体而言,模板肯定是优点远大于缺点的。

相关文章
|
1月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
95 10
|
17天前
|
自然语言处理 编译器 Linux
|
23天前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
1月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
103 21
|
1月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
16 1
|
1月前
|
Linux 编译器 C语言
Linux c/c++之多文档编译
这篇文章介绍了在Linux操作系统下使用gcc编译器进行C/C++多文件编译的方法和步骤。
39 0
Linux c/c++之多文档编译
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
41 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
9天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4