欢迎光临
我们一直在努力

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

📌 相关专栏

  • 【Linux专栏】
  • 【C语言专栏】
  • 【测试专栏】
  • 【MySQL专栏】
  • 【C++ 专栏】

📌 相关文章推荐

  • 【C++】 深入容器适配器:从零实现stack与queue
  • 【C++】 String 核心用法:构造、遍历、容量与优化技巧
  • 【C++】手写双向链表:list容器模拟实现
  • 【C++】C++继承入门(上):继承语法与基本特性详解

举爪爪打招呼

很高兴你点开这篇文章✨

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

👍 点赞 ⭐ 收藏 💬 评论


文章目录

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

前言

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

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

1. 非类型模板参数

1.1 基本概念

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

// 固定数组大小的 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)

template<class T, double N> //:error C7592: “double”类型的非类型模板参数至少需要“std:c++20”,这里是C++17
//:必须是整型(int、size_t等)
class Stack {};


1.3 非类型参数 vs 宏定义

  • 宏定义:所有数组大小都是 N,不能按需使用
  • 非类型参数:每个实例化可指定不同大小

#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 arr[10]std::array<int, 10>
内存位置 栈/全局
大小信息 传参时退化丢失 保留大小信息
范围 for 传参后不能用 始终可用
越界检查 仅警告(读不检查) operator[] 断言检查
容器接口 有迭代器等

2.2 传参时的区别

  • 原生数组传参:退化为指针,丢失大小信息
    • 不能使用范围 for(不知道大小)
  • array 传参:保留完整类型信息
    • 可以使用范围 for

// 原生数组传参:退化为指针,丢失大小信息
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 模式)

void test1()
{
int a1[10];
array<int, 10> a2;

// 普通数组:不检查越界读,输出随机垃圾值
cout << a1[10] << endl; // 越界读,只会报警告

// array:重载 operator[],会进行断言检查
// cout << a2[10] << endl; // 越界断言(debug 模式)
}


3. 模板特化概述

3.1 为什么需要特化?

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

  • 比如用模板比较指针时,默认会比较“地址”而非指针“指向的内容” —— 这时候就需要 “模板特化”,为特殊类型写专属逻辑,即特化是用来解决“特殊类型”适配问题的

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 特化的语法

// 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 特化的步骤

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

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

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

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


  • 4.2 特化的陷阱

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

    // ❌ 错误: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 全特化的语法

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

    // 通用类模板
    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 优先级规则

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

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


    6. 类模板偏特化

    6.1 部分参数特化

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

    // 通用版本
    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 匹配规则

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


    7. 指针与引用的偏特化

    7.1 指针类型的偏特化

    // 两个参数偏特化为指针类型
    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 引用类型的偏特化

    // 两个参数偏特化为引用类型
    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)

    void test4()
    {
    // 匹配指针偏特化版本
    Data<char*, double*> d4;
    d4.func(); // 打印:char double(原类型,不是 char* double*)

    // 匹配引用偏特化版本
    Data<char&, double&> d5;
    d5.func(); // 打印:char double
    }


    8. 完整测试用例

    8.1 非类型参数测试

    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 函数模板特化测试

    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 类模板特化测试

    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

  • 欢迎留言交流 ​
  • 期待你的评论与建议 ​
  • 留下你的想法吧

  • 举爪爪求关注

    谢谢你看到这里呀

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

    👍 点赞 ⭐ 收藏 💬 评论

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址