【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • [1. 非类型模板参数](#1. 非类型模板参数)
    • [1.1 基本概念](#1.1 基本概念)
    • [1.2 使用限制](#1.2 使用限制)
    • [1.3 非类型参数 vs 宏定义](#1.3 非类型参数 vs 宏定义)
  • [2. array:非类型参数的典型应用](#2. array:非类型参数的典型应用)
    • [2.1 array 与原生数组的对比](#2.1 array 与原生数组的对比)
    • [2.2 传参时的区别](#2.2 传参时的区别)
    • [2.3 越界检查对比](#2.3 越界检查对比)
  • [3. 模板特化概述](#3. 模板特化概述)
    • [3.1 为什么需要特化?](#3.1 为什么需要特化?)
    • [3.2 特化的概念](#3.2 特化的概念)
  • [4. 函数模板特化](#4. 函数模板特化)
    • [4.1 特化的语法](#4.1 特化的语法)
    • [4.2 特化的步骤](#4.2 特化的步骤)
    • [4.2 特化的陷阱](#4.2 特化的陷阱)
    • [4.3 函数模板特化的建议](#4.3 函数模板特化的建议)
  • [5. 类模板全特化](#5. 类模板全特化)
    • [5.1 全特化的语法](#5.1 全特化的语法)
    • [5.2 优先级规则](#5.2 优先级规则)
  • [6. 类模板偏特化](#6. 类模板偏特化)
    • [6.1 部分参数特化](#6.1 部分参数特化)
    • [6.2 匹配规则](#6.2 匹配规则)
  • [7. 指针与引用的偏特化](#7. 指针与引用的偏特化)
    • [7.1 指针类型的偏特化](#7.1 指针类型的偏特化)
    • [7.2 引用类型的偏特化](#7.2 引用类型的偏特化)
    • [7.3 指针偏特化的价值](#7.3 指针偏特化的价值)
  • [8. 完整测试用例](#8. 完整测试用例)
    • [8.1 非类型参数测试](#8.1 非类型参数测试)
    • [8.2 函数模板特化测试](#8.2 函数模板特化测试)
    • [8.3 类模板特化测试](#8.3 类模板特化测试)
  • [9. 总结](#9. 总结)
  • 本文代码链接

前言

在基础模板学习中,我们常用的模板参数是 类型形参(如 template)。但实际开发中,有时还需要给模板传递编译期常量(如固定数组大小、指定缓存容量),这时 非类型模板参数 就派上用场了。

  • 此外,当通用模板逻辑对某些特殊类型(如指针)不适用时,就需要 模板特化 来为这些类型编写专属逻辑。本文将从这两个方向深入讲解 C++ 模板的进阶特性。

1. 非类型模板参数

1.1 基本概念

非类型模板参数是用编译期可确定的常量作为模板参数,在模板内部可以直接当常量使用。

cpp 复制代码
// 固定数组大小的 Stack 类
template<class T, size_t N = 0> 	//:error C7592: "double"类型的非类型模板参数至少需要"std:c++20"
									//:必须是整型(int、size_t等)
class Stack
{
private:
    T _arr[N];   // N 是编译期常量,作为数组大小
};

1.2 使用限制

限制 说明
允许的类型 整型(int、size_t、char、bool 等)
C++20 前 不支持 double、float 等浮点类型
C++20 起 支持 double(需指定 /std:c++20)
cpp 复制代码
template<class T, double N>   		//:error C7592: "double"类型的非类型模板参数至少需要"std:c++20",这里是C++17
									//:必须是整型(int、size_t等)
class Stack {};

1.3 非类型参数 vs 宏定义

  • 宏定义:所有数组大小都是 N,不能按需使用
  • 非类型参数:每个实例化可指定不同大小
cpp 复制代码
#define N 10   						// 宏定义:所有数组大小都是 10,不能按需使用
template<class T, size_t N = 0>  	// 非类型参数:每个实例化可指定不同大小

class Stack {};

Stack<int> st0;      // N = 0
Stack<int, 5> st1;   // N = 5
Stack<int, 10> st2;  // N = 10(相当于实例化出不同的类)

优势 :非类型模板参数更加灵活,可以为不同实例指定不同大小,而宏定义是全局统一的。


2. array:非类型参数的典型应用

2.1 array 与原生数组的对比

std::array 是 C++11 引入的静态数组容器,使用非类型模板参数固定大小:

特性 原生数组 int arr10 std::array<int, 10>
内存位置 栈/全局
大小信息 传参时退化丢失 保留大小信息
范围 for 传参后不能用 始终可用
越界检查 仅警告(读不检查) operator\[\] 断言检查
容器接口 有迭代器等

2.2 传参时的区别

  • 原生数组传参:退化为指针,丢失大小信息
    • 不能使用范围 for(不知道大小)
  • array 传参:保留完整类型信息
    • 可以使用范围 for
cpp 复制代码
// 原生数组传参:退化为指针,丢失大小信息
void func(int* a)
{
    // ❌ 不能使用范围 for(不知道大小)
    // for (auto e : a) { }
}

// array 传参:保留完整类型信息
void func(array<int, 10>& a)
{
    // ✅ 可以使用范围 for
    for (auto e : a)
    {
        cout << e << " ";
    }
}

2.3 越界检查对比

  • 普通数组:不检查越界读,输出随机垃圾值
    • 越界读,只报警告
  • array:重载 operator ,会进行断言检查
    • 越界断言(debug 模式)
cpp 复制代码
void test1()
{
    int a1[10];
    array<int, 10> a2;
    
    // 普通数组:不检查越界读,输出随机垃圾值
    cout << a1[10] << endl;   //  越界读,只会报警告
    
    // array:重载 operator[],会进行断言检查
    // cout << a2[10] << endl;   //  越界断言(debug 模式)
}

3. 模板特化概述

3.1 为什么需要特化?

模板的核心是 通用,但遇到特殊类型时,通用逻辑可能失效:

  • 比如用模板比较指针时,默认会比较"地址"而非指针"指向的内容" ------ 这时候就需要 "模板特化",为特殊类型写专属逻辑,即特化是用来解决"特殊类型"适配问题的
cpp 复制代码
template<class T>
bool Less(T left, T right)
{
    return left < right;
}

// 问题:比较指针时,比较的是地址而非指向的值
int* p1 = &i1;
int* p2 = &i2;
cout << Less(p1, p2) << endl;   // 比较地址,不是 11 < 10

3.2 特化的概念

特化为特殊类型编写专属逻辑,分为两类

  • 特化 :所有模板参数都被指定为具体类型
  • 偏特化(半特化):部分模板参数被特化,或添加指针/引用等限制

4. 函数模板特化

4.1 特化的语法

cpp 复制代码
// 1. 基础函数模板
template<class T>
bool Less(const T& left, const T& right)
{
    return left < right;
}

// 2. 全特化语法(不推荐,问题较多)
template<>
bool Less<int*>(int* const& left, int* const& right)
{
    return *left < *right;
}

4.2 特化的步骤

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

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

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

  4. 函数形参表:必须要和模板函数的"基础参数类型"完全相同(后面会进行详细讲解),如果不同编译器可能会报一些奇怪的错误。


4.2 特化的陷阱

函数模板特化容易出现问题,推荐使用普通函数重载:

cpp 复制代码
// ❌ 错误:const 修饰的歧义
// bool Less<int*>(const int*& left, const int*& right)  // 与模板不匹配

// ✅ 正确:普通函数重载(推荐)
bool Less(int* left, int* right)
{
    return *left < *right;
}

// 处理 const int* 类型
bool Less(const int* left, const int* right)
{
    return *left < *right;
}

4.3 函数模板特化的建议

核心规则 :当普通函数和模板函数都能匹配调用时,编译器会优先选择普通函数。因此,直接编写普通函数重载即可,无需使用函数模板特化。


5. 类模板全特化

5.1 全特化的语法

全特化是将类模板的所有参数都指定为具体类型 :

cpp 复制代码
// 通用类模板
template<class T1, class T2>
class Data
{
public:
    Data() { cout << "Data<T1, T2>" << endl; }
};

// 全特化:T1 = int, T2 = double
template<>
class Data<int, double>
{
public:
    Data() { cout << "Data<int, double> 全特化" << endl; }
    void func() {}   // 特化版本可以新增方法
};

5.2 优先级规则

当同时存在全特化和偏特化时,编译器优先选择最匹配的版本:

cpp 复制代码
void test3()
{
    Data<int, int> d1;      // 通用版本(无匹配特化)
    Data<int, double> d2;   // 全特化版本
    Data<char, double> d3;  // 偏特化版本(后面会讲)
}

6. 类模板偏特化

6.1 部分参数特化

偏特化可以只特化部分模板参数:

cpp 复制代码
// 通用版本
template<class T1, class T2>
class Data {};

// 偏特化:只固定第二个参数为 double
template<class T1>
class Data<T1, double>
{
public:
    Data() { cout << "Data<T1, double> 偏特化" << endl; }
    void func() { cout << typeid(T1).name() << endl; }
};

6.2 匹配规则

cpp 复制代码
Data<int, int> d1;      // 通用版本(第二个参数不是 double)
Data<char, double> d3;  // 偏特化版本(第二个参数是 double)

7. 指针与引用的偏特化

7.1 指针类型的偏特化

cpp 复制代码
// 两个参数偏特化为指针类型
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
    Data()
    {
        cout << "Data<T1*, T2*> 偏特化--参数更进一步限制" << endl;
        
        // 这里可以获取到原类型 T1、T2(而非指针类型)
        T1 i = 0;        // T1 是原类型
        T1* pi = &i;
    }
    
    void func()
    {
        cout << typeid(T1).name() << endl;  // 打印原类型名
        cout << typeid(T2).name() << endl;
    }
};

7.2 引用类型的偏特化

cpp 复制代码
// 两个参数偏特化为引用类型
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
    Data()
    {
        cout << "Data<T1&, T2&> 偏特化--参数更进一步限制" << endl;
    }
    
    void func()
    {
        cout << typeid(T1).name() << endl;  // 打印引用绑定的类型
        cout << typeid(T2).name() << endl;
    }
};

7.3 指针偏特化的价值

价值 :可以同时获取指针类型(T*)和原类型(T)

cpp 复制代码
void test4()
{
    // 匹配指针偏特化版本
    Data<char*, double*> d4;
    d4.func();  // 打印:char double(原类型,不是 char* double*)
    
    // 匹配引用偏特化版本
    Data<char&, double&> d5;
    d5.func();  // 打印:char double
}

8. 完整测试用例

8.1 非类型参数测试

cpp 复制代码
void test1()
{
    Stack<int> st0;       // N = 0(缺省值)
    Stack<int, 5> st1;    // N = 5
    Stack<int, 10> st2;   // N = 10(三个不同的类)
    
    int a1[10];
    array<int, 10> a2;
    
    func(a1);   // 传参退化,不能用范围 for
    func(a2);   // 保留类型,可以用范围 for
}

8.2 函数模板特化测试

cpp 复制代码
void test2()
{
    int i1 = 11, i2 = 10;
    cout << Less(i1, i2) << endl;        // 0(11 < 10?false)
    
    int* p1 = &i1, * p2 = &i2;
    cout << Less(p1, p2) << endl;        // 0(比较指向的值)
    
    const int* p3 = &i1, * p4 = &i2;
    cout << Less(p3, p4) << endl;        // 0(const 版本)
}

8.3 类模板特化测试

cpp 复制代码
void test5()
{
    Data2<int, int> d1;           // 通用版本
    
    Data2<char*, double*> d4;     // 指针偏特化版本
    d4.func();                    // 打印:char double
    
    Data2<char*, double> d6;      // 部分偏特化(第二个参数 double)
    d6.func();                    // 打印:char
}

9. 总结

C++ 模板的进阶特性:

分类 核心内容
非类型模板参数 编译期常量作为模板参数,用于固定数组大小等
array 容器 非类型参数的典型应用,比原生数组更安全、更易用
函数模板特化 语法复杂,推荐使用普通函数重载替代
类模板全特化 所有参数指定具体类型
类模板偏特化 部分参数特化,或添加指针/引用限制

特化优先级 :普通函数(非模板) > 全特化 > 偏特化 > 通用模板

场景 推荐使用
固定大小容器 使用非类型模板参数
函数针对特殊类型处理 使用普通函数重载
类针对特殊类型处理 使用偏特化(更灵活)
需要同时获取指针和原类型 使用指针偏特化

本文代码链接

https://gitee.com/ayidyy/cyvyan11/commit/03f32164959d87fa54e627b99cad2d246daed3a5

  1. 欢迎留言交流
  2. 期待你的评论与建议
  3. 留下你的想法吧

谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论

相关推荐
自传.2 小时前
尚硅谷 Vibe Coding|第二章 AI编程工具生态 学习笔记
笔记·学习·ai编程·尚硅谷·vibe coding
代码改善世界2 小时前
【C++进阶】C++11:列表初始化、右值引用与移动语义、完美转发全解析
java·开发语言·c++
scx_link2 小时前
通过git bash在本地创建分支,并推送到远程仓库中
开发语言·git·bash
GZ同学2 小时前
单双变量Ripley’s K函数 R 语言实现
开发语言·r语言
Channing Lewis2 小时前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel
牛油果子哥q2 小时前
并查集(DSU)超精讲,路径压缩、按秩合并、万能模板、连通性判定、最小生成树与刷题实战全解
数据结构·c++·最小生成树·并查集
小小龙学IT2 小时前
Apache Airflow 2.x 深度指南:用 Python 编排一切的现代化工作流引擎
开发语言·python·apache
少爷晚安。2 小时前
Java基础02_JDK&JRE下载安装及环境配置
java·开发语言
库奇噜啦呼2 小时前
【iOS】RunLoop学习
学习·ios