c++ 模板

一、什么是模板(Template)

模板是 C++ 支持的泛型编程机制,可以用来编写与类型无关的代码,实现代码的复用。编译器根据调用时提供的类型参数生成对应版本代码,称为模板实例化


二、函数模板(Function Template)

  • 模板函数在编译时 根据传入参数类型自动生成对应函数代码(实例化),普通函数是编写时确定类型。
  • 模板函数可以避免为不同类型写多个重载版本。

语法

cpp 复制代码
template<typename T>
返回类型 函数名(参数列表) {
    // 使用类型T的代码
}

或者:

cpp 复制代码
template<class T>
返回类型 函数名(参数列表) {
    // 使用类型T的代码
}

typenameclass 在这里是等价的。


示例:通用交换函数

cpp 复制代码
template<typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

使用:

cpp 复制代码
int x = 1, y = 2;
mySwap(x, y);  // 自动推导T为int

double a = 1.1, b = 2.2;
mySwap(a, b);  // 自动推导T为double

三、类模板(Class Template)

语法

cpp 复制代码
template<typename T>
class ClassName {
public:
    T data;
    void print();
};
cpp 复制代码
template<typename T>
class MyClass {
public:
    void show();
};

template<typename T>
void MyClass<T>::show() {
    std::cout << "Show: " << typeid(T).name() << std::endl;
}

四、模板参数多样性

模板不仅可以是类型,还可以是非类型参数:

非类型参数可以是整数、指针、引用、布尔值等

cpp 复制代码
template<typename T, int size>
class Array {
    T arr[size];
public:
    void printSize() {
        std::cout << "Size: " << size << std::endl;
    }
};

也可以是多个参数:

cpp 复制代码
template<typename T1, typename T2>
void printPair(T1 a, T2 b) {
    std::cout << a << " and " << b << std::endl;
}

//使用
printPair(1, 2.5);  // 输出:1 and 2.5

也可以是可变参数:

C++11 引入,支持接收任意数量的参数

cpp 复制代码
//打印所有参数
void print() { }  // 终止递归

template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...);  // 递归展开
}

//使用
print(1, 2.5, "Hello");  // 输出:1 2.5 Hello

五、模板特化(Template Specialization)

有时你希望对特定类型的模板做定制化处理

一、类模板特化

cpp 复制代码
template<typename T>
class Printer {
public:
    void print(const T& value) {
        std::cout << "General: " << value << std::endl;
    }
};

// 对 std::string 类型进行特化
template<>
class Printer<std::string> {
public:
    void print(const std::string& value) {
        std::cout << "String: \"" << value << "\"" << std::endl;
    }
};

使用:

cpp 复制代码
Printer<int> p1;
p1.print(123);  // General: 123

Printer<std::string> p2;
p2.print("Hello");  // String: "Hello"

二、函数模板特化

cpp 复制代码
template<typename T>
void printValue(T value) {
    std::cout << "Generic: " << value << std::endl;
}

template<>
void printValue<char>(char value) {
    std::cout << "Char (int): " << static_cast<int>(value) << std::endl;
}

三、模板递归(用于编译期计算)

示例:计算阶乘(编译期)

cpp 复制代码
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

使用:

cpp 复制代码
int result = Factorial<5>::value;  // 120

四、模板默认参数

cpp 复制代码
template<typename T = int>
class MyContainer {
    T value;
public:
    MyContainer(T v) : value(v) {}
    void show() { std::cout << value << std::endl; }
};

使用:

cpp 复制代码
MyContainer<> c1(100);         // 使用默认int
MyContainer<double> c2(3.14);  // 指定double

五、模板别名(C++11)

cpp 复制代码
template<typename T>
using Vec = std::vector<T>;

Vec<int> nums;  // 相当于 std::vector<int>

六、SFINAE(Substitution Failure Is Not An Error)

允许根据类型特征选择不同实现(高级技巧)

cpp 复制代码
template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type
check(T t) {
    std::cout << "Integral type\n";
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value>::type
check(T t) {
    std::cout << "Floating point type\n";
}

七、模板中的 decltypeauto自动推导

cpp 复制代码
template<typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
    return a + b;
}

八、小结对照表

模板技巧 作用/用途
函数模板 定义通用函数
类模板 定义通用类
模板特化 为特定类型提供特殊实现
非类型参数 指定大小、常量等
递归模板 编译时递归计算,如阶乘
可变参数模板 接收任意数量类型参数
SFINAE 启用/禁用特定模板实例(用于类型选择)
auto + decltype 自动推导返回类型

函数模板声明为什么放在头文件中

在 C++ 中,函数模板与类模板 的定义和声明有一个重要区别 于普通函数或类:模板的定义必须可见于调用点,否则编译器在实例化时找不到它的实现。下面是详细解释与示例:


普通函数 vs 函数模板

类型 声明和定义可以分离吗? 实现可以放 .cpp 文件吗?
普通函数 ✅ 可以 ✅ 可以
函数模板 ❌ 不推荐分离 ❌ 不建议放在 .cpp 文件

为什么函数模板不建议将定义放在 .cpp

因为模板的代码是在实例化(即使用)时生成 的,编译器必须看到定义才能生成代码。这意味着:

  • 模板的定义通常要放在头文件中;
  • 如果只写了声明放在头文件,而定义放在 .cpp,链接时会报错 undefined reference

正确方式:函数模板定义写在头文件

my_template.hpp

cpp 复制代码
#ifndef MY_TEMPLATE_HPP
#define MY_TEMPLATE_HPP

#include <iostream>

template<typename T>
void printValue(const T& val) {
    std::cout << val << std::endl;
}

#endif

使用:

cpp 复制代码
#include "my_template.hpp"

int main() {
    printValue(42);           // 输出:42
    printValue("hello");      // 输出:hello
}

错误方式:声明在 .hpp,定义在 .cpp

my_template.hpp

cpp 复制代码
#ifndef MY_TEMPLATE_HPP
#define MY_TEMPLATE_HPP

template<typename T>
void printValue(const T& val);  // 只有声明

#endif

my_template.cpp

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

template<typename T>
void printValue(const T& val) {
    std::cout << val << std::endl;
}

这段代码编译时不会出错,但链接时报错

复制代码
undefined reference to `printValue<int>(int const&)`

因为模板定义在 .cpp,在被 main.cpp 使用时不可见,无法实例化。


如果你一定要分离定义和声明?

你可以使用 include "xxx.tpp" 的方式,将定义放在单独文件中,但最终仍然要包含进 .hpp 文件中。

my_template.hpp

cpp 复制代码
#ifndef MY_TEMPLATE_HPP
#define MY_TEMPLATE_HPP

template<typename T>
void printValue(const T& val);  // 声明

#include "my_template.tpp"      // 引入定义

#endif

my_template.tpp

cpp 复制代码
template<typename T>
void printValue(const T& val) {
    std::cout << val << std::endl;
}

这种方式是 Boost、Eigen 等大型模板库常用的技巧。


类模板也是一样的原则

类模板也必须将定义放在头文件中。

cpp 复制代码
// MyClass.hpp
template<typename T>
class MyClass {
public:
    void show();
};

template<typename T>
void MyClass<T>::show() {
    std::cout << "Show" << std::endl;
}