C++模板

July 24, 2025 作者: funnywii 分类: 编程 浏览: 6 评论: 0

C++是一门强类型语言,编写一个通用函数,能把任意类型的变量传进去处理,通过把通用逻辑设计为模板来摆脱类型限制

C++中的模板语法,实际上是在为C++提供泛型(Generic Programming)的机制。

最常见的泛型应用在STL的容器中。

类模板

Class Templates用于生成类。

没有必要纠结模板类类模板二者的差异,只要记住,只有Class Templates,没有Templates Class,但是你怎么称呼它无所谓。当有人秃噜嘴了说出“模板类”的时候,实际上是在说:a template for a class​^{[4]}。此外,C++的创造者鲁迅Bjarne Stroustrup在描述Templates时也说:

There are people who make semantic distinctions between the terms class template and template class. I don't; that would be too subtle: please consider those terms interchangeable. Similarly, I consider function template interchangeable with template function.

类模板的声明,定义和实例化

类模板可以用关键字 templatetypename来声明和定义。先看声明:

template <typename T> 
class ClassName;

以及定义:

template <typename T> 
class ClassName {
    // 类定义
};

template关键字说明接下来我们会定义一个模板,模板和函数类似,也有一个参数列表,不同的是模板类的参数列表使用 <>括起来,T表示数据类型的占位符,class也是个关键字。

假设我们定义一个动态数组 MyVec类:

template <typename T> 
class MyVec{
public:
    void push_back(T const&);
    void clear();  

private:
    T* elements;
};

随后可以实例化为double型和int型的动态数组:

MyVec<int> IntVec;
MyVec<double> DoubleVec;

IntVec.push_back(42);  
DoubleVec.push_back(3.14); 

但是类模板实例化和普通类是不同的,编译器需要知道 T 类型才能实例化出实际的类。

MyVec WrongInst;  # 错误的方式

当然一个Class Template也可以接受多个参数,在这里还指定了一个默认的 int类型:

template <class T, class U, class V = int> 
class ClassName {
  private:
    T member1;
    U member2;
    V member3;
    ... .. ...
  public:
    ... .. ...
};

ClassName<char, float> obj1('A', 1.23f);   # 使用默认的模板参数V=int
ClassName<std::string, double, long> obj2("text", 3.14, 100L); # 显式指定所有类型

类模板的成员函数

C++ 是静态类型语言,对象的类型在编译期确定,编译器不会为未实例化的模板生成任何代码。只有当模板实例化后,才能确定模板类及其成员所占用的空间大小。

模板类在调用成员函数时,需要同时看到完整的成员函数声明和定义,因此模板类中的成员函数通常以内联方式实现。不过定义部分放在类型之外也是可以的:

template <class T> 
class ClassName {
    void functionName();
};

template <class T>
void ClassName<T>::functionName() {
    // code
}

一定不能写成下面的样子,会找不到泛型 T符号。

ClassName::functionName(){
    // code
}

此外,成员函数的实现并不一定非得在同一个 hpp文件中​^{[6]},但是将其分离至 hppcpp没有什么必要。。。

函数模板

函数模板的声明,定义和调用

函数模板和类模板的语法相同。也是以关键字 template和模板参数列表 <>作为函数模板的声明与定义的开始。

template <typename T> 
void foo(T const& v);
# e.g.
int x = 42;
foo(x); 

能够接受一个任意类型 T的常量引用并返回void。

template <typename T> 
T foo();
# e.g.
int x = foo<int>();  // 必须显式指定类型 T

不接受参数,返回任意类型 T的值。

template <typename T, typename U> 
U foo(T const&);
# e.g.
double d = foo<int, double>(42);  // 必须显式指定 U,T 可推导

接受一个任意类型 T的常量引用并返回类型 U的值。

template <typename T> 
void foo(){
    T var;
    // ...
}
# e.g.
foo<int>();

不接受参数,内部创建类型 T的变量。

自动类型推断

要知道,编译器不能根据返回值来推断类型,因为函数调用时,不知道谁会接受返回值。

template <typename T>
T create() {
    return T();
}

int main() {
    auto x = create();  # 编译报错
    auto y = create<int>();  # 正确
}

这里的auto关键字只是简化写法,实际类型由模板参数 T通过类型推断确定。

此外,模板还能够实现部分显式指定、部分自动推断。但是编译器对模板参数的顺序有限制:必须先写显式指定的模板参数,再写自动推断的模板参数。

// 模板参数U需要显式指定,T可从参数推导
template <typename U, typename T> U convert(T value) {
    return static_cast<U>(value);
}

// 错误:T需推导,但放在了U(需指定)的前面
template <typename T, typename U> U bad_convert(T value) {
    return static_cast<U>(value);
}

int main() {
    // 显式指定U为double,T从参数42推导为int
    double x = convert<double>(42);  
    // 编译错误
    double y = bad_convert<double>(42); 
}

非类型模板参数

可以向模板传递非 typename类型参数:

template <class T, int max> int arrMin(T arr[], int n){
    int m = max;
    for (int i = 0; i < n; i++)
        if (arr[i] < m)
            m = arr[i];
    return m;
}

typename类型参数只能为 int型,且必须是编译期常量。

可变参数模板(C++11)

能对参数进行高度泛化,能表示0到任意个数、任意类型的参数。

语法是在 typenameclass 关键字后加上 ...,这种实现方式叫做参数包Parameter Pack。省略号的作用是声明一个参数包 T..args,这个参数可以包含零到任意个模板参数。参数包底层以类似数组的形式存储 ,但是语法不支持使用 args[i]的方式获取可变参数,只能通过展开参数包(遍历)的方式获取参数包中的每个参数。

参数包有两种:

  • 模板参数包:表示零个或多个模板参数
  • 函数参数包:表示零个或多个函数参数
// Args是一个模板参数包;rest是一个函数参数包,它是Args这个类型参数包所对应的函数参数
// Args表示零个多多个模板类型参数
// rest表示零个或多个函数参数
 
template<typename T,typename... Args>
void foo(const T &t, const Args& ... rest){
    //...
}

int main(){
    int i = 0;
    double d = 3.14;
    string s = "how now brown cow";
    // T的类型从第一个实参推断出来,其余实参从后面的实参中推断
    foo(i, s, 42, d); //包含三个参数
    foo(s, 42, "hi"); //包含二个参数
    foo(d, s);        //包含一个参数
    foo("hi");        //空包
 
    return 0;
}

如果想要知道包中有多少元素,可以用 sizeof运算符。

template<typename... Args>
void func(Args... args) {
    std::cout << sizeof...(Args) << std::endl; //模板参数的数目
    std::cout << sizeof...(args) << std::endl; //函数参数的数目
}

处理参数包可以使用递归调用的方法,使用模板特化和递归展开来逐个处理参数。

  • 主模板:处理至少1个参数的情况,把第一个参数和剩余参数包分离
  • 种植模板:处理最后1个参数,是递归终止条件,以避免无限递归
  • 递归展开:每次调用递归,参数包参数数量-1
// 1. 终止函数:处理最后一个参数
template<typename T>
void print(const T& t) {
    std::cout << t << std::endl;
}
// 2. 递归函数:处理至少一个参数的情况
template<typename T, typename... Args>
void print(const T& first, const Args&... args) {
    std::cout << first << ", ";  // 处理当前参数
    print(args...);             // 递归处理剩余参数包
}
int main() {
    print(1, 2.5, "hello", std::string("world"));
    // 输出:1, 2.5, hello, world
}

第1次调用时,first=1args...=2.5, "hello", std::string("world")

第2次调用时,first=2.5args...="hello", std::string("world")

...

最后一次调用,print(std::string("world"));

变量模板(C++14)

首先,变量模板不是变量,只有实例化的变量模板,编译器才会生成实际的变量,变量模板实例化后是一个全局变量,但是同一个模板生成不同类型的类、函数、变量,彼此之间没有半毛钱的关系,连地址也不一样。

变量模板让我们可以在使用变量时指定这个变量的类型。一般会带上 constexpr 关键字(附带了 const),目的是让编译器在编译时能够确定变量类型,而非运行时。

#include <iostream>
using namespace std;
// Template variable with constexpr
template <typename T> 
constexpr T pi = T(3.14159265359);

int main(){
    // Using pi with different types
    cout << "Pi as float: " << pi<float> << endl;
    cout << "Pi as double: " << pi<double> << endl;
    return 0;
}

模板全特化和偏特化

全特化

全特化的模板参数列表应当是空的。类模板,函数模板,变量模板都可以全特化。通俗的理解,全特化就是给特性类型“开小灶:,当模板对某个具体类型不适用或低效,可以专门为这个类型写一套实现。

  1. 模板函数的全特化

比如一个通用打印函数 print

// 通用模板:处理大多数类型
template <typename T>
void print(T data) {
    std::cout << "General Print:" << data << std::endl;
}

如果T为string类型,想加上引号打印:

// 全特化:专门处理string类型
template <>
void print<std::string>(std::string data) {
    std::cout << "String Print:\"" << data << "\"" << std::endl;
}

在调用时:

print(123);        // 用通用模板:输出“General Print:123”
print("hello");    // 用全特化版本:输出“String Print:"hello"”
  1. 类模板的全特化

通用的类模板相当于一个万能钥匙,能处理大多数情况,但不够灵活;全特化类模板就是给某个特殊类型做一把专用钥匙,能够实现一些专属功能。

// 通用模板:适合大多数类型
template <typename T>
class Storage {
private:
    T data;
public:
    void save(T value) { data = value; }
    T get() { return data; }
};

// 如想获取data长度,通用模板不能满足
// 全特化:专门给string类型定制
template <>
class Storage<std::string> {
private:
    std::string data;
public:
    void save(std::string value) { data = value; }
    std::string get() { return data; }
    // 专属功能:计算长度
    int length() { return data.size(); }
};
  1. 变量模板的全特化

以上面的代码为例:

template <typename T> 
constexpr T pi = T(3.14159265359);

如果想给 int类型定制一个 ​\pi

template <>
constexpr int pi<int> = 3;

输出为:

int i_pi = pi<int>;        // int特化版,结果为3

但是如果使用通用模板,int的输出也是3,这涉及到了浮点到整形的隐式转换,小数点被直接截断了,属于被动转换,但是全特化的方式是主动的。

偏特化

模板偏特化给某一类类型(相比单个具体类型)“批量定制” 实现。

值得注意的是函数模板不允许偏特化。

  1. 类模板的偏特化
// 通用类模板:处理普通类型
template <typename T>
class Printer {
public:
    void print(T value) {
        std::cout << "普通类型值:" << value << std::endl;
    }
};

// 偏特化:处理指针类型 T*
template <typename T>
class Printer<T*> {  // 这里的<T*>是对模板参数的限制,只匹配指针类型
public:
    void print(T* value) {
        if (value) {
            std::cout << "指针指向的值:" << *value << std::endl;
        } else {
            std::cout << "指针为空!" << std::endl;
        }
    }
};


// main函数
Printer<int> p1;
p1.print(123);  // 通用版本输出“普通类型值:123”

int num = 456;
Printer<int*> p2;  // 匹配指针类型偏特化版本
p2.print(&num);    // 输出“指针指向的值:456”

不用为每个具体指针类型(int*double*等)单独写全特化。

  1. 变量模板的偏特化
// 通用模板:大多数类型的默认容量是10
template <typename T>
constexpr size_t default_capacity = 10;

// 偏特化1:所有vector容器(不管存什么类型)默认容量是100
template <typename T>
constexpr size_t default_capacity<std::vector<T>> = 100;

// 偏特化2:所有array容器(不管存什么类型、多大尺寸)默认容量是其固定大小
template <typename T, size_t N>
constexpr size_t default_capacity<std::array<T, N>> = N;


//main函数
    // 普通类型:用通用模板,容量10
    std::cout << "int的默认容量: " << default_capacity<int> << "\n";  // 输出10

    // vector容器:用偏特化1,容量100(不管存int还是double)
    std::cout << "vector<int>的默认容量: " << default_capacity<std::vector<int>> << "\n";       // 输出100
    std::cout << "vector<double>的默认容量: " << default_capacity<std::vector<double>> << "\n"; // 输出100

    // array容器:用偏特化2,容量是其固定大小(N)
    std::cout << "array<int, 5>的默认容量: " << default_capacity<std::array<int, 5>> << "\n";   // 输出5
    std::cout << "array<char, 20>的默认容量: " << default_capacity<std::array<char, 20>> << "\n"; // 输出20

在通用变量模板下,对大多数类型,默认容量都是10。

对于所有vector容器,无论其存储什么类型变量,容量都是100。

对于array容器,获取其固定大小。

参考文章

[1] Introduction · C++ Template Tutorial

[2] Templates in C++ - GeeksforGeeks

[3] C++ Class Templates

[4] c++ - What is the difference between a template class and a class template? - Stack Overflow

[5] 原理:C++为什么一般把模板实现放入头文件 - 同勉共进 - 博客园

[6] C++模板的定义是否只能放在头文件中?_c++模版定义必须放在头文件吗-CSDN博客

[7] 现代 C++ 模板教程

[8] 泛化之美--C++11可变模版参数的妙用 - qicosmos(江南) - 博客园

[9] 省略号和可变参数模板 | Microsoft Learn

[10] C++:52---可变参数模板

#C++(27)

评论