【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?

【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?
前言:在之前的文章中,我们介绍了模板的基础知识,包括函数模板和类模板的使用方法。本文将深入探讨模板的进阶内容,涵盖非类型模板参数、模板特化以及模板的分离编译等高级特性。
在这里插入图片描述
🌟 专注用图文结合拆解难点+代码落地知识,让技术学习从「难懂」变“一看就会”!
🏠 个人主页MSTcheng · ZEEKLOG
💻 代码仓库MSTcheng · Gitee📚 精选专栏 :📖 :《C语言》🧩 :《数据结构》💡 :《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!”

文章目录

一、非类型模板参数

1.1模板参数的分类

首先要知道模板参数分为类型模板参数和非类型模板参数,在前面的文章中我们介绍了类型模板参数 例如:
template<classT>classDate{public:voidprint(){ cout << _year <<"-"<< _month <<"-"<< _day;}private: T _year=2025; T _month=11; T _day=20;};intmain(){ Date<int> d; d.print();return0;}
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

🔺T类型的私有成员在该日期类实例化对象的时候就实例化出了具体类型,比如上面示例中的int。所以类型模板参数是在实例化的时候才确定类型的

1.2非类型模板参数的认识

那什么是非类型模板参数呢?🤔

非类型模板参数(Non-Type Template Parameters)是C++模板中允许使用的非类型值作为参数,即在编译时确定的常量表达式。与类型模板参数(如typename T)不同,非类型模板参数可以是整型、枚举、指针、引用或std::nullptr_t等具体值

下面举个例子:

#defineN10//静态的栈template<classT>classstack{public:stack():_a(nullptr),_top(0),_capacity(0){}private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//可以控制N为10 创建一个空间为10的静态栈 stack<int> s2;//也可以控制N为100创建一个空间为100的静态栈return0;}
我们可以通过宏定义来控制静态栈的空间大小,但这种方法存在明显局限——所有栈实例只能使用相同的预设大小,无法实现不同栈拥有不同容量(例如一个栈10个元素,另一个栈100个元素这时小的那个栈就会浪费90个空间)。为解决这个问题,可以通过引入非类型模板参数来灵活控制各个栈的独立容量
//使用整型做非模板参数//非模板参数:使用一个正数说常量作为类模板的一个参数 //使用整型来做类模板的参数 这里的N为10是缺省值 传了值就用传的那个值 没传就用缺省值 template<classT=int, size_t N =10>classstack{private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//10个空间 stack<int,100> s2;//100个空间 stack<int,1000> s3;//1000个空间 cout <<sizeof(s1)<< endl; cout <<sizeof(s2)<< endl; cout <<sizeof(s3)<< endl;return0;}
这段代码利用非类型模板参数N来动态调整栈空间大小。 相比宏定义,模板参数提供了更大的灵活性,使每个栈实例都能拥有独立的存储空间。这种设计既确保了空间分配的确定性,又有效避免了资源浪费问题。

🔺注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。

1.3array容器

在我们之前学过的容器中,如string、vector、stack和queue,都很少使用非类型模板参数。而array容器则是个例外,它采用了非类型模板参数。既然提到了这个概念,我们不妨来了解一下它。
在这里插入图片描述


在这里插入图片描述
该容器提供的接口与vector类似,迭代器也采用原生指针实现,因此不再赘述接口的使用方法。我们将重点探讨array与传统C语言数组的主要区别:
//---------------------静态数组 与array的区别#include<array>intmain(){//array容器 array<int,10> a1;//静态数组int a2[10]={0};//静态数组的越界访问  a2[0]=2; a2[9]=100;//访问这两个越界的地方没有报错 说明静态数组越界访问不会报错 cout << a2[10]<< endl; cout << a2[15]<< endl;//将越界处的数据修改会报错 说明静态数组越界 写(修改)会报错检查不出来//a2[11] = 23;//a2[15] = 24;for(auto e : a2){ cout << e <<" ";} cout << endl;//越界写入数据 会报错 a1[10]=2; cout << a1[10]<< endl;//越界访问读取数据 也会报错 由此得出array比静态数组 检查的更严格 静态数组只是抽查 cout << a1[15]<< endl;return0;}

🔺需要注意的是,array之所以能够检测数组越界读写,关键在于其重载的operator[]函数内部实现了对传入下标的边界检查机制

二、模板的特化

2.1什么是特化?

模板特化是C++中针对泛型编程的一种机制,允许为特定类型或条件提供定制化的模板实现。当通用模板无法满足某些特殊类型的需求时,可以通过特化来优化或改变其行为。相当于是模板的特殊处理。

2.2什么场景下使用特化?

下面请看例子:

template<classT>boolLess(const T& left,const T& right){return left < right;}intmain(){int a =1, b =2; cout <<Less(a,b)<<" "<<endl; cout <<Less(2,3)<< endl;int* pa =&a;int* pb =&b; cout <<Less(pb, pa)<< endl;double* p1 =newdouble(2.2);double* p2 =newdouble(1.1); cout <<Less(p1, p2)<< endl;//这里明明p1指向的内容比p2大 //传值过去判断应该为假才对 但是输出结果是1//所以就要对函数模板进行特殊化 即生成一种专门应对这种情况的模板 string* p3 =newstring("111"); string* p4 =newstring("222"); cout <<Less(p3, p4)<< endl;//把p3和p4传递过去后 模板就实例化了一个string*的类型//而left和right拿到的是p3和p4的地址 所以比较的就是地址 而不是内容return0;}
🤨从上述代码可见,Less函数模板虽然能处理大多数情况但在比较指针类型时存在局限:它比较的是指针本身而非指针指向的内容。 因此,我们需要对模板进行特化处理来解决这个问题。

对于上面函数模板的特化版本如下:

//特化template<>//template<>表示这是一个显式特化bool Less<double*>(double*const& left,double*const& right){return*left <*right;}

注意:

🔺特化的模板需要显示实例化例如:Less<double*>表明对模板类或模板函数Less进行特化,特化的类型是double*(即指向double的指针)。
🔺特化后的模板参数不能写成const double* & ! 因为主模板是const T&修饰的是形参本身不能被修改,如果T=double*就边成const double* &修饰的是指针指向的内部不能被修改而不是指针本身不能被修改!所以特化模板和主模板参数不匹配!会报错!
📢最初的模板的参数一定要与特化后函数的参数对应匹配 !!!

在这里插入图片描述
const T* p1 ——>const在*的左边修饰指向的内容 内容不能改 *p1(解引用)不能修改 T const* p2——>const在*的左边修饰指向的内容 内容不能改 *p2(解引用)不能修改 *const p3 ——>const在*的右边修饰指针本身 本身的指向不能改 p3(指针本身)不能修改 

如果涉及较多的指针内容的比较我们也可写成一个通过模板——>前提:要有主模板

template<classT>boolLess( T*const& left, T*const& right){return*left <*right;}

2.3特化的种类

模板的特化分为两种:全特化、偏特化。

1、全特化

全特化👉指参数列表中所有的参数都确定!

//---------------------------------全特化-------------------------------//主类模板template<classT1,classT2>classData{public:Data(){ cout <<"Data<T1,T2>"<< endl;}private: T1 _d1; T2 _d2;};//特化类模板template<>classData<int,char>{public:Data(){ cout <<"Data(int,char)"<< endl;}private:int _d1;char _d2;};intmain(){ Data<int,char> d1;//调用特化的模板 Data<int,int>d2;//调用主模板 cout <<typeid(d1).name()<< endl; cout <<typeid(d2).name()<< endl;return0;}
在这里插入图片描述


2、偏特化

偏特化👉就是指定部分参数。

//------------------------------偏特化-------------------------------template<classT1,classT2>classData{public:Data(){ cout <<"Data<T1,T2>"<< endl;}voidf1(){};};// 偏特化// 特化部分参数template<classT1>classData<T1,char>{public:Data(){ cout <<"Data<T1, char>"<< endl;}voidf1(){};};// 对参数进一步限制template<classT1,classT2>classData<T1*, T2*>{public:Data(){ cout <<"Data<T1*, T2*>"<< endl;}voidf1(){ T1 x1; cout <<typeid(x1).name()<< endl; T1* x2; cout <<typeid(x2).name()<< endl;}};intmain(){ Data<int,int> d1;//调用主模板 d1.f1(); Data<int,char> d2;//调用偏特化模板 d2.f1(); Data<char,char> d3;//调用偏特化模板 Data<char*,char*> d4; Data<int*,char*> d5; Data<double*,double*> d6; d4.f1(); Data<double&,double&> d7; Data<double*,double&> d8;return0;}
在这里插入图片描述

这里可能会有人有疑问:d4中使用一个T定义了一个x1,使用T*定义了一个x2使用typeid打印出来的为什么一个是char一个是char*?

因为T1 x1声明了一个非指针变量,其类型为T1(即指针T1*所指向的底层类型)。
T1* x2声明了一个指针变量,其类型为T1*(即原始的模板参数类型)。

举个例子:
若实例化Data<int*, double*>:

T1被推导为intT2被推导为double
T1 x1;中的x1类型为int
T1* x2;中的x2类型为int*

三、模板的分离编译

3.1什么是模板的分离编译?

模板分离编译👉指将模板的声明和实现分别放在不同的文件中(通常是头文件.h和源文件.cpp),类似于普通函数的声明与实现分离。这种设计初衷是为了提高代码的可维护性和编译效率。

3.2模板的分离编译

假设有下面两个函数,他们的声明和定义均分别放在.h文件(声明)和.cpp文件(定义)中:

<在Func.h文件中>#include<iostream>usingnamespace std;//模板函数的声明template<classT>voidFuncT(const T& x);//普通函数的声明voidFuncF();
<在Func.cpp文件中>#include"Func.h"//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//普通函数的定义voidFuncF(){ cout <<"普通函数:FuncF()"<< endl;}
<在test.cpp文件中调用这两个函数>#include"Func.h"intmain(){//函数模板调用FuncT(1);//call FuncT(?)找不到FuncT的地址//普通函数调用FuncF();//call FuncF(普通函数的地址)return0;}
在这里插入图片描述


在这里插入图片描述


为什么会报链接错误呢?这也是我们在前面STL各种容器的模拟实现中强调类模板声明和定义最好不要分离(分文件)的原因!这其实是没有实例化造成的,下面画个图让大家更加直观的理解:

在这里插入图片描述

3.3链接错误的解决方案

上面我们已经分析了,编译器报链接错误是由于函数模板没有实例化造成的,那么要解决该问题我们就要从实例化入手!

方法一:在func.cpp文件中显示实例化

//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//显示实例化template<>voidFuncT(constint& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}
这种实例化方式有较为明显的局限性——>每实例化一种类型就要人为的去显示实例化,既增加了工作量也增加了代码的冗余度所以这种方法并不推荐。

方法二:将声明和定义放在同一个文件例如“xxx.h”文件或"xxx.hpp"文件

<在.h文件中>template<classT>voidFuncT(const T& x){ cout <<"void FuncT(const T& x)"<< endl;}//类模板也类似template<classT>classStack{public://类里面定义voidPush(const T& x);};//类外面定义template<classT>voidStack<T>::Push(const T& x){ cout <<"void Push(const T& x)"<< endl;}
这种方法明显优于第一种既减少了工作量,也减少了代码冗余度。推荐使用这种方法!

四、模板总结

💡【优点】

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

💡【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

Read more

《MySQL 事务深度解析:从 ACID 到实战,守住数据一致性的最后防线》

《MySQL 事务深度解析:从 ACID 到实战,守住数据一致性的最后防线》

前引:数据是业务的核心,而事务是数据可靠性的 “守护神”。在 MySQL 中,事务看似简单的 “提交 / 回滚” 操作,背后藏着 ACID 特性的严格约束、隔离级别的底层实现,以及并发场景下的锁竞争逻辑。很多开发者因为一知半解,导致系统出现脏读、幻读、数据丢失等严重问题。今天,我们就来层层拆解 MySQL 事务,让你从 “会用” 到 “精通”,真正守住数据一致性的底线! 目录 【一】事务介绍 【二】为什么要有事务 【三】事务的版本支持 【四】事务提交的两种方式 【五】事务的几种操作 (1)开始一个事务 (2)创建一个保存点 (3)回滚到指定保存点 (4)正常结束一个事务 (5)异常结束一个事务

By Ne0inhk
Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案

Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案 前言 在前文中,我们探讨了 okay 在鸿蒙(OpenHarmony)端实现基础 Result 模式包装的实战。但在真正的“分布式微服务聚合”、“高并发资产对账”以及“具备自愈能力的 IoT 指令链”场景中。简单的 ok() 与 err() 判定往往不足以支撑起复杂的业务全景。面对需要同时并行发起 3 个 API 请求,并要求在“所有请求均成功时执行合并、任一请求失败时执行局部逻辑路由”的高阶需求。如果缺乏一套完善的异步结果映射与多级逻辑聚合机制。不仅会导致异步回调地狱(Callback Hell)在

By Ne0inhk
Spring Boot 数据访问与数据库集成

Spring Boot 数据访问与数据库集成

Spring Boot 数据访问与数据库集成 18.1 学习目标与重点提示 学习目标:掌握Spring Boot数据访问与数据库集成的核心概念与使用方法,包括Spring Boot数据访问的基本方法、Spring Boot与MySQL的集成、Spring Boot与H2的集成、Spring Boot与MyBatis的集成、Spring Boot与JPA的集成、Spring Boot的事务管理、Spring Boot的实际应用场景,学会在实际开发中处理数据库访问问题。 重点:Spring Boot数据访问的基本方法、Spring Boot与MySQL的集成、Spring Boot与H2的集成、Spring Boot与MyBatis的集成、Spring Boot与JPA的集成、Spring Boot的事务管理、Spring Boot的实际应用场景。 18.2 Spring Boot数据访问概述 Spring Boot数据访问是指使用Spring Boot进行数据库操作的方法。 18.2.1 数据访问的定义

By Ne0inhk
RUST:异步代码的测试与调试艺术

RUST:异步代码的测试与调试艺术

RUST:异步代码的测试与调试艺术 一、异步测试的本质与难点 1.1 异步测试与同步测试的区别 💡在Rust同步编程中,测试通常是顺序执行的,每个测试函数会阻塞线程直到完成,结果是确定的。而异步测试的结果可能受到任务调度、网络延迟、数据库连接等因素的影响,时序性和状态管理更加复杂。 同步测试示例: #[cfg(test)]modtests{#[test]fntest_add(){assert_eq!(1+1,2);}} 异步测试示例(使用Tokio测试宏): #[cfg(test)]modtests{usetokio::time::sleep;usestd::time::Duration;#[tokio::test]asyncfntest_async_add(){sleep(Duration::from_millis(100)).await;assert_

By Ne0inhk