C++模板进阶

前言

大家好!今天我们来聊聊C++模板的高级特性。如果你已经掌握了基本的模板用法,那么这篇文章将带你深入了解非类型模板参数、模板特化以及模板分离编译等进阶内容。

一、非类型模板参数

1.1 什么是非类型模板参数?

模版参数分两种:

1:类型模板参数 :用class或者typename 定义的类型参数

cpp 复制代码
template<class T>
T add(T a, T b)
{
	return a + b;
}
int main()
{
   cout<<add<int>(5, 5);
}

2:非类型模板参数:用一个常量作为模板参数,可以当常量使用

简单来说,非类型模板参数就是传值而不是传类型

cpp 复制代码
#include <iostream>
#include<vector>
using namespace std;

// 非类型模板参数
template<size_t N=10>  // 👈 重点:这就是【非类型模板参数】
class Stack {
public:
	int _a[N];         // 👈 用 N 作为数组大小
	int _top;
};

int main()
{
	Stack<> s1;    // N 用默认值 10
	Stack<20> s2;  // N 显式传 20
}
  • template<size_t N> 是【非类型模板参数】,传的是数字
  • 传不同值 → 生成不同的类
  • Stack<> = 使用默认值 10

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。

  2. 非类型的模板参数必须在编译期就能确认结果

二、模板特化 ------ "特殊的人特殊对待"

通俗解释:

模板是通用的,但有些特殊类型需要"单独处理",否则会出错。

特化就是为某种类型写一个专用版本

生活例子:

你写了一个"做饭"模板,普通人就是"洗米煮饭"。

但是如果是"婴儿",就需要"打成泥"。

这就是特化。

1. 函数模板特化

下面是一个比较大小的函数:

cpp 复制代码
 template<typename T>
 bool Less(const T& a, const T& b)
 {
     return a < b;
 }
int main()
{
    int a = 10;
    int b = 20;
    cout << Less(a, b) << endl;
    cout << Less(55.5, 8.9)<< endl;
    cout << Less(&a, &b);//错误
}

它比较 intdoubleDate 对象都没问题。

但如果你传的是指针,它比较的是地址,而不是对象本身,就会出错。

解决方法1:函数模版特化

cpp 复制代码
  template<>
  bool Less<int*>(int*const&a,int*const&b)
  {
      return *a<*b;
  }

这样,当传 地址 时,就会走这个特化版本。

解决方法2:直接重载(简单明了)

cpp 复制代码
   bool Less(int *A,int*B)
   {
       return *A<*B;
   }

加<const char*>类型的函数模版特化:

cpp 复制代码
//   函数模板特化
     template<typename T>
     bool Less(const T& a, const T& b)
     {
         return a < b;
     }

     template<>
     bool Less<int*>(int*const&a,int*const&b)
     {
         return *a<*b;
     }

     template<>
     bool Less<const char*>(const char* const &a,const char* const &b)
     {
         return strcmp(a, b) < 0;
     }

     int main()
     {
         int a = 10;
         int b = 20;
         cout << Less(a, b) << endl;
         cout << Less(55.5, 8.9)<< endl;
         
         //字符串常量的类型是 const char *   如果不特化,则会调用默认的 Less<T> 函数模板
         cout << Less("hello", "world")<< endl;

         cout << Less(&a, &b);

         return 0;
     }

cout << Less("hello", "world") << endl;

  • "hello""world" 的类型都是 const char[6],退化后为 const char*

  • 有一个特化版本 Less<const char*>(const char* const &a, const char* const &b)

  • 使用 strcmp(a, b) < 0 进行比较

  • strcmp("hello", "world") 返回负数(因为 'h' < 'w'),所以结果为真

  • 输出:1

注意:Less("hello", "world"); hello是字符串常量,它的类型是const char*, 即指针指向的内容不能改, 因为模板参数是const T& a, 所以模板实例化过程是 const char* &, 再加上是const引用,所以类型是 const char * const &

翻译成人话就是:

对 "const char 类型指针" 的 const 引用*

  • 指针本身不能改(右边 const)
  • 指针指向的内容也不能改(左边 const)
  • 用引用传递,不拷贝

总结:

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板

  2. 关键字template后面接一对空的尖括号<>

  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型

  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇 怪的错误。

函数模板特化:就是全特化,没有偏特化

建议: 函数模板特化不如直接函数重载,不推荐特化函数模板

2. 类模板特化

全特化(所有参数都特化)

就是给所有模板参数都指定具体类型,把模板彻底变成一个普通函数。改变模板的行为逻辑

特点:

  • template<> 里面空了
  • 所有模板参数都给定了
  • 这叫全特化
cpp 复制代码
#include <iostream>
using namespace std;

// 基础模板
template<class T1, class T2>
class Data {
public:
    Data() { cout << "Data<T1, T2> 基础模板" << endl; }
private:
    T1 _d1;
    T2 _d2;
};

// 全特化:指定 T1=int, T2=char
template<>
class Data<int, char> {
public:
    Data() { cout << "Data<int, char> 全特化版本" << endl; }
private:
    int _d1;
    char _d2;
};

int main() {
    Data<double, double> d1;  // 基础模板
    Data<int, int> d2;        // 基础模板
    Data<int, char> d3;       // 全特化版本
    
    return 0;
}
偏特化(部分特化)(C++规定偏特化只有类模板能玩!函数模板玩不了)

偏特化 = 只给一部分模板参数指定类型,剩下的还是模板参数

cpp 复制代码
#include <iostream>
using namespace std;

// 基础模板
template<class T1, class T2>
class Data {
public:
    Data() { cout << "Data<T1, T2> 基础模板" << endl; }
};

// 偏特化1:第二个参数固定为 int
template<class T1>
class Data<T1, int> {
public:
    Data() { cout << "Data<T1, int> 偏特化版本(第二个参数为int)" << endl; }
};

// 偏特化2:两个参数都是指针
template<class T1, class T2>
class Data<T1*, T2*> {
public:
    Data() { cout << "Data<T1*, T2*> 偏特化版本(指针类型)" << endl; }
};

// 偏特化3:两个参数都是引用
template<class T1, class T2>
class Data<T1&, T2&> {
public:
    Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2) {
        cout << "Data<T1&, T2&> 偏特化版本(引用类型)" << endl;
    }
private:
    const T1& _d1;
    const T2& _d2;
};

int main() {
    Data<double, double> d1;      // 基础模板
    Data<double, int> d2;         // 偏特化1
    Data<int*, double*> d3;       // 偏特化2
    Data<int&, int&> d4(1, 2);    // 偏特化3
    
    return 0;
}

偏特化 = 只针对 "部分类型 / 特定结构" 做特殊处理

比如:

  • 主模板:Data<T1, T2>
  • 偏特化:Data<T1&, T2&>

意思是:

只要你传的两个类型都是引用,就用这个偏特化版本!

cpp 复制代码
 Data<int&, int&> d4(1, 2);    // 偏特化3
偏特化的核心特点:

1.模板参数列表不空

cpp 复制代码
template<class T1, class T2>   // 还有模板参数 → 偏特化

2.只限制部分格式 这里限制:必须是两个引用

cpp 复制代码
Data<T1&, T2&>

**3.只有类模板能写偏特化!**函数模板不能这么写!

全特化,偏特化总结:

  • 类模板
    • 可以 全特化
    • 可以 偏特化
  • 函数模板
    • 可以 全特化
    • 不能偏特化

三、模板分离编译

3.1 问题演示

add.h(声明)

cpp 复制代码
#pragma once

template<class T>
T Add(const T& left, const T& right);

add.cpp(定义)

cpp 复制代码
#include "add.h"

template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

main.cpp(使用)

cpp 复制代码
#include "add.h"
#include <iostream>

int main() {
    std::cout << Add(1, 2) << std::endl;  // 链接错误!
    return 0;
}

编译结果: 链接错误!找不到 Add<int> 的实现。

3.2 原因分析

模板不能分离编译,是因为模板程序在编译过程中需要经过两次编译:

cpp 复制代码
// 第一次编译:检查模板本身的语法
template<class T>
T Add(const T& left, const T& right) {
    return left + right;  // 只检查语法,不检查 left + right 是否合法
}

// 第二次编译:实例化时检查具体类型
// 当使用 Add(1, 2) 时,编译器生成 Add<int> 的代码
// 此时才检查 int +

第一次编译(编译 add.cpp 时):

  • 检查语法:template<class T> 写法正确吗?

  • 检查括号匹配:{} 对吗?

  • 不检查 a + b 是否合法(因为不知道 T 是什么)

第二次编译(编译 main.cpp 时):

  • 遇到 Add(1, 2),确定 T = int

  • 把 T 替换成 int,生成代码:

    cpp 复制代码
    int Add(const int& a, const int& b) {
        return a + b;  // 现在检查:int + int 合法 ✅
    }

    这次编译需要看到函数体! 而分离编译模式下,定义在 .cpp 文件中对其他文件不可见。

3.3 解决方案

方案1:将声明和定义放在一起(推荐)

add.hpp

cpp 复制代码
#pragma once

template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

main.cpp

cpp 复制代码
#include "add.hpp"
#include <iostream>

int main() {
    std::cout << Add(1, 2) << std::endl;        // 3
    std::cout << Add(1.5, 2.7) << std::endl;    // 4.2
    std::cout << Add<string>("Hello", " World") << endl;  // Hello World
    return 0;
}
方案2:显式实例化(不推荐)
复制代码
显式实例化(不是全特化),没有template<>,不改变行为: 仍然使用基础模板的实现

add.cpp

cpp 复制代码
#include "add.h"

template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

// 显式实例化
template int Add<int>(const int& left, const int& right);
template double Add<double>(const double& left, const double& right);

显式实例化就是明确告诉编译器:编译器,我不管你看到没看到使用,反正你给我生成 Add<int> 的代码!"

编译 main.cpp 时,编译器只知道需要 Add<int>,但不知道 add.cpp 里有显式实例化。

编译 add.cpp 时,编译器看到显式实例化指令 ,所以生成代码。
链接时,两者才能配合起来。"

缺点: 每次添加新类型都要手动实例化,非常麻烦!

四、最佳实践建议

  1. 非类型模板参数 :优先使用 size_t 类型,确保编译时常量

  2. 模板特化 :类模板推荐特化,函数模板推荐重载

  3. 分离编译 :模板代码全部放在头文件中(.hpp.h

  4. 命名规范 :模板参数用 TUV 或语义化名称

  5. 编译器选中:全特化 > 偏特化 > 基础模板

  6. 错误处理 :善用 static_assert 进行编译期检查

编译器总是选择"最符合"当前类型的特化版本,全特化最优先,偏特化次之,基础模板兜底!

相关推荐
无敌昊哥战神1 小时前
【保姆级题解】力扣17. 电话号码的字母组合 (回溯算法经典入门) | Python/C/C++多语言详解
c语言·c++·python·算法·leetcode
脱氧核糖核酸__1 小时前
LeetCode热题100——238.除了自身以外数组的乘积(题目+题解+答案)
数据结构·c++·算法·leetcode
ouliten2 小时前
C++笔记:std::invoke
c++·笔记
坐吃山猪2 小时前
Python27_协程游戏理解
开发语言·python·游戏
gCode Teacher 格码致知2 小时前
Javascript提高:小数精度和随机数-由Deepseek产生
开发语言·javascript·ecmascript
椰猫子2 小时前
Javaweb(Filter、Listener、AJAX、JSON)
java·开发语言
j_xxx404_3 小时前
C++算法:哈希表(简介|两数之和|判断是否互为字符重排)
数据结构·c++·算法·leetcode·蓝桥杯·力扣·散列表
盛世宏博北京3 小时前
以太网温湿度传感器运维技巧,提升设备稳定性与使用寿命
开发语言·php·以太网温湿度传感器
代码改善世界3 小时前
【MATLAB初阶】矩阵操作(一)
开发语言·matlab·矩阵