C++泛型编程——模板

目录

💫一、模板的底层本质:编译期代码生成

💫二、核心分类:函数模板和类模板

✨1、函数模板:通用函数的实现

1️⃣基本语法

2️⃣类型推导

3️⃣模板参数和非类型参数

4️⃣函数模板的重载(也就是函数模板的全特化)

✨2、类模板:通用类的实现

1️⃣基本语法:

2️⃣实例化:

3️⃣类模板的特化

💫三、一些模板的高级特性

1️⃣模板的递归与编译期计算

2️⃣模板参数的默认值

3️⃣可变参数模板(C++11及之后的版本)

💫四、模板的优缺点

1️⃣优点:

2️⃣缺点:


C++ 模板是泛型编程的核心机制,它允许开发者编写与具体类型无关的代码,通过 “参数化类型” 实现代码的复用。模板不是直接编译执行的代码,而是编译器生成具体代码的 “蓝图”,编译器会根据实际传入的类型参数自动生成对应版本的函数或类。

下面从其底层原理、分类、特性以及高级用法来进行讲解:

💫一、模板的底层本质:编译期代码生成

模板的核心是 “类型参数化”,也就是把代码中具体的类型(比如intdouble)抽象为一个 “类型变量”(通常用T表示)。当代码中使用模板时,编译器会执行以下操作:

  1. 检查模板语法是否合法(比如括号匹配、关键字正确);
  2. 根据传入的类型参数(如intstd::string),生成一份针对该类型的具体代码(称为 “模板实例化”);
  3. 对生成的具体代码进行编译,会和普通代码一样参与链接。

示例:

#include <iostream>

template <typename T>
T add(T a, T b) { return a + b; }

int main() {
    auto n = add(1, 2);       // 编译器生成 add<int>(int, int)
    auto m = add(1.5, 2.5);   // 编译器生成 add<double>(double, double)
    std::cout << "n:" << n << std::endl; // 输出:3
    std::cout << "m:" << m << std::endl; // 输出:4
}
  • template 关键字开始模板声明
  • <typename T> <class T> 定义模板参数,T 是类型参数(一般还是推荐使用 typename)
  • T 可以在函数中像普通类型一样使用

上述代码中,add函数模板会被实例化为两个函数:add<int>add<double>,分别处理intdouble类型的加法。

💫二、核心分类:函数模板和类模板

✨1、函数模板:通用函数的实现
  • 目标:定义可以处理多种类型的通用函数,避免因为不同类型而重复编写逻辑相同的函数,提高代码的复用性。
  • 特点通过参数类型推导自动生成具体函数版本,可以支持重载和非类型参数
1️⃣基本语法
template <typename T>  // 模板参数声明(typename可替换为class)
返回值类型 函数名(参数列表) {
    // 函数体(使用T作为类型)
}

2️⃣类型推导

🔥①自动推导类型:调用函数模板时,编译器会根据实参类型自动推断T的类型(无需显式指定)

template <typename T>
void print(T value) { 
    std::cout << typeid(T).name() << std::endl;
    std::cout << value << std::endl; 
}

print(100);       // 推导T=int,生成print<int>
print("hello");   // 推导T=const char*,生成print<const char*>

❓❓❓为什么“hello”会被推导为const char * 而不是std::string呢?

第一点:模板类型推导遵循精确匹配原则。字符串字面量 "hello" 在 C++ 中的类型是 const char[6](包含结尾的 \0),在函数参数传递时会退化为 const char*

第二点:模板类型推导期间不会进行用户定义的隐式转换。即使 const char* 可以隐式转换为 std::string,但是这发生在函数调用时,而不是模板推导时。

🔥②显式指定类型:当推导失败(比如参数类型不匹配)时,需显式指定T

template <typename T>
void printSize(T value) {
    std::cout << "Size: " << sizeof(value) << std::endl;
}

int main() {
    int x = 42;
    
    printSize(x);           // T = int, size = 4
    printSize<double>(x);   // 显式指定:T = double, size = 8
                            // 强制进行 int -> double 转换
}

3️⃣模板参数和非类型参数
  • 类型参数:即typename T中的T,代表一种数据类型(如intdouble,或者自定义类)。
  • 非类型参数:编译期已知的常量(如整数、指针、引用),用于指定固定属性(如数组大小)。
// 非类型参数N:指定数组大小(编译期常量)
template <typename T, int N>
void printArray(T (&arr)[N]) {  
    for (int i = 0; i < N; ++i) {
        std::cout << arr[i] << std::endl;
    }
}

int main() {
    int arr1[3] = {1, 2, 3};
    printArray(arr1);  // 推导T=int,N=3,生成printArray<int, 3>

    double arr2[2] = {1.1, 2.2};
    printArray(arr2);  // 推导T=double,N=2,生成printArray<double, 2>
}

非类型参数必须是编译期常量(如字面量、constexpr变量),不能是运行时变量。

4️⃣函数模板的重载(也就是函数模板的全特化)

函数模板可以与普通函数、其他模板重载,编译器按 “最佳匹配” 原则选择调用版本:

  • 全特化优先级高于模板(前提是精确匹配时)
  • 模板间按 “更特例化” 原则选择(通俗的讲,就是如果模板 A 的适用范围是模板 B 适用场景的 “子集”,也就是A的生效范围小于B的,则 A 比 B 更特例化,优先被选择。比如Tconst T,后者针对性更强,更特例化)。
// 函数模板:处理通用类型
template <typename T>
void swap(T& a, T& b) {
    std::cout << "模板交换" << std::endl;
    T temp = a;
    a = b;
    b = temp;
}

// 函数模板的全特化:处理int类型
template <>
void swap(int& a, int& b) {
    std::cout << "普通int交换" << std::endl;
    std::swap(a, b);
}


int main() {
    int x = 1, y = 2;
    swap(x, y);  // 调用全特化版本(精确匹配)

    double a = 1.1, b = 2.2;
    swap(a, b);  // 调用模板(推导T=double)
}

✨2、类模板:通用类的实现

类模板允许定义通用的类,通过类型参数化,实现不同类型的对象。

1️⃣基本语法:
template <typename T>  // 模板参数声明
class ClassName {
private:
    T member;  // 用T定义成员变量
public:
    ClassName(T val) : member(val) {}  // 构造函数
    T get() const { return member; }  // 成员函数
};

// 类外定义成员函数(需重复模板参数)
template <typename T>
void 类名<T>::set(T val) {
    member = val;
}

2️⃣实例化

使用类模板时必须显式指定类型参数(因为编译器无法推导)

// 实例化:生成Class<int>类
ClassName<int> intObj(10);
std::cout << intObj.get() << std::endl;  // 输出10

// 实例化:生成Class<std::string>类
ClassName<std::string> strObj("hello");
std::cout << strObj.get() << std::endl;  // 输出hello

要点:

  • 类模板可以有多个类型参数。
  • 模板参数可以被用于成员变量和成员函数中。
  • 类模板实例化时指定具体类型。

3️⃣类模板的特化

当通用模板对某些类特殊类型不适用时,可通过 “特化” 提供自定义实现(分为全特化偏特化)。

全特化

为特定类型完全定制模板实现。它提供了模板的一个特定版本,当模板参数完全匹配特化类型时,编译器将优先使用该特化版本。

// 通用模板
template <typename T>
class Printer {
public:
    void print(T val) { 
        std::cout << "通用打印:" << val << std::endl; 
    }
};

// 对bool类型的全特化
template <>  //!全特化无模板参数
class Printer<bool> {
public:
    void print(bool val) {
        std::cout << "bool特化打印:" << (val ? "true" : "false") << std::endl;
    }
};

int main() {
    Printer<int> intP;
    intP.print(123);  // 通用打印:123

    Printer<bool> boolP;
    boolP.print(true);  // bool特化打印:true
}

解析

  • 通用模板适用于所有类型,在print函数中以通用方式输出值。
  • 全特化模板针对bool类型进行了专门化,实现了不同的print函数。
  • 当实例化Printer<bool>时,编译器选择全特化版本而非通用模板。

偏特化

偏特化允许模板对部分参数进行特定类型的处理,同时又保持其他参数的通用性。

对于类模板而言,可以针对模板参数的某些特性进行偏特化;对于函数模板,则仅支持全特化,不支持偏特化。

// 多参数通用模板
template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;
    Pair(T1 f, T2 s) : first(f), second(s) {}
};

// 偏特化:第一个参数为int
template <typename T2>  // 仅特化T1=int,T2仍为模板参数
class Pair<int, T2> {
public:
    int first;
    T2 second;
    Pair(int f, T2 s) : first(f), second(s) {
        std::cout << "int特化Pair:" << f << ", " << s << std::endl;
    }
};

int main() {
    Pair<double, std::string> p1(1.5, "hello");  // 通用模板
    Pair<int, double> p2(10, 3.14);  // 偏特化模板(T1=int)
}

解析

  • 偏特化模板处理第一个类型为int的情况.
  • 使用偏特化提升了模板的灵活性,使其能够根据部分参数类型进行不同处理。

💫三、一些模板的高级特性

1️⃣模板的递归与编译期计算


模板可通过递归实现编译期计算(利用模板实例化的递归展开),是constexpr的前身。

示例:编译期计算阶乘

template <int N>
struct Factorial {
    static constexpr int value = N * Factorial<N-1>::value;  // 递归
};

// 递归终止条件(全特化)
template <>
struct Factorial<0> {
    static constexpr int value = 1;
};

int main() {
    constexpr int f5 = Factorial<5>::value;  // 编译期计算:5! = 120
    std::cout << f5 << std::endl;  // 输出120
}

2️⃣模板参数的默认值


模板参数(类型参数或非类型参数)可指定默认值,使用时若不传入则采用默认值。

// 类型参数默认值:默认T为int
template <typename T = int>
class Stack {
private:
    std::vector<T> v;
public:
    void push(T elem) { v.push_back(elem); }
};

// 非类型参数默认值:默认Size为10
template <typename T, int Size = 10>
class Array {
private:
    T data[Size];
};

int main() {
    Stack<> intStack;  // 采用默认T=int
    Stack<std::string> strStack;  // 显式指定T=std::string

    Array<int> arr1;  // 采用默认Size=10
    Array<double, 5> arr2;  // 显式指定Size=5
}

3️⃣可变参数模板(C++11及之后的版本)

支持任意数量的模板参数(类型参数或非类型参数),用于实现灵活的函数或类(如std::tupleprintf风格函数)。

示例:可变参数模板函数

// 递归终止函数(无参数)
void print() {
    std::cout << std::endl;
}

// 可变参数模板:展开第一个参数,递归处理剩余参数
template <typename T, typename... Args>  // Args为参数包
void print(T first, Args... rest) {  // 展开第一个参数first
    std::cout << first << std::endl;
    print(rest...);  // 递归处理剩余参数
}

int main() {
    print(1, 2.5, "hello", true);  // 输出:1 2.5 hello 1 
}

💫四、模板的优缺点

1️⃣优点:
  • 代码复用性强:一套模板适配多种类型,避免重复编写相似代码(如std::vector支持所有类型)。
  • 类型安全:编译期检查类型兼容性,比void*等 C 风格泛型更安全。
  • 零运行时开销:模板实例化在编译期完成,生成的代码与手写具体类型代码效率一致(无动态多态的虚函数调用开销)。
2️⃣缺点:
  • 编译时间长:模板实例化会生成大量代码,增加编译时间。
  • 错误信息复杂:模板错误提示冗长(如 “无法推导模板参数”),调试难度大。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值