【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. 出现模板编译错误时,错误信息非常凌乱,且准确度不高 (不要轻易去相信模板的报错),不易定位错误。可能只是一个小错误,却报出一大串的错误 (深有体会),此时一定要优先看第一个错误。

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

相关文章
|
10月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
298 0
|
自然语言处理 编译器 C语言
为什么C/C++编译腰要先完成汇编
C/C++ 编译过程中先生成汇编语言是历史、技术和实践的共同选择。历史上,汇编语言作为成熟的中间表示方式,简化了工具链;技术上,分阶段编译更高效,汇编便于调试和移植;实践中,保留汇编阶段降低了复杂度,增强了可移植性和优化能力。即使在现代编译器中,汇编仍作为重要桥梁,帮助开发者更好地理解和优化代码。
为什么C/C++编译腰要先完成汇编
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
1108 56
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
737 22
|
Linux 编译器 C语言
Linux c/c++之多文档编译
这篇文章介绍了在Linux操作系统下使用gcc编译器进行C/C++多文件编译的方法和步骤。
235 0
Linux c/c++之多文档编译
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
308 2
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
12月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
467 12
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
251 0