别再被模板折磨了:非类型参数 + 特化 + 分离编译一网打尽


个人主页

🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库

🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础

⛺️Per aspera ad astra.

在这里插入图片描述



1. 非类型模板参数

模板参数分为非类型形参类型形参

1.1. 类型形参

类型当作形参传给模板,举个例子:食谱里写“食材=任意肉类”,那么编译时再填“鸡肉/牛肉/羊肉”

// 函数模板 T是类型形参
template <typename T>
T myMax(const T& a, const T& b){
    return a < b ? b : a;
}
// 类模板 T是类型形参
template <class T>
    struct Boxx{ T val;};

怎么用?编译器根据实参推导,详情请见这篇文章:初识C++模板-优快云博客

补充:

1. 可以有**缺省参数** `template <class T = int> struct X{ };`
2. 可以**多形参** `template <class A, class B> struct Pair;`
3. 可以模板模板参数(容器套容器)
template <template<class...> class Container, class T>
struct Wrapper { Container<T> c; };
Wrapper<std::vector, int> w;

1.2.非类型模板参数

编译期就能确定的值当作形参传给模板,举个例子:食谱里写“份数=3”,编译时就知道做三人份,锅多大都订好了

这些值在编译期就能确定,可以影响类型的组成生成的代码

C++20之前,只允许整型做非类型模板参数

C++20之后,支持double等内置类型,但是最常用的还是整型参数

最常见的应用:数组/缓冲区大小

C++11增加了array容器,std::array<T,N>固定长度、连续内存的STL容器,大小在编译期确定,零额外开销封装了T[N],同时提供了标准容器接口,本质是一个静态数组

它有以下特性:

  1. 类型即尺寸:std::array<int,4>std::array<int,5>是不同的类型,N是非类型模板参数,编译期作为常量
  2. 固定容量:不可增删元素,没有动态分配
  3. array读写进行严格的越界检查,但是对于普通数组,越界检查是一种抽查,检查边界的临近位置, 并且只能检查越界写

在这里插入图片描述

在这里插入图片描述

以下是非类型模板参数的代码演示:

// 1. 非类型模板参数
// 定义一个模板类型的静态数组
template<typename T, size_t size = 10>
class Array {
public:
	T& operator[](size_t index) { return _array[index]; }
	const T& operator[](size_t index) const { return _array[index]; }
	size_t size() const { return _size; }
	bool empty() const { return _size == 0; }
private:
	T _array[size];
	size_t _size;
};

2. 模板的特化

使用模板可以实现一些与类型无关的代码,但是有些特殊类型可能会得到错误结果,需要特殊处理,这个就是模板的特化

例如:实现一个用于小于比较的函数模板

// 实现一个小于比较的函数模板
template<class T>
bool Less(const T& left,  const T& right) { return left < right; }

class Date {
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 2025, int month = 10, int day = 15)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }
	bool operator<(const Date& other) const noexcept {
		if (_year < other._year) return true;
		if (_year > other._year) return false;

		if (_month < other._month) return true;
		if (_month > other._month) return false;

		return _day < other._day; 
	}
};

int main() {
	cout << Less(1, 2) << endl; // 正确
	cout << Less('X', 'Y') << endl; // 正确
	
	Date d1(2025, 10, 20);
	Date d2(2025, 10, 15);
	cout << Less(d1, d2) << endl; // 比较正确

	// 这里比较的是地址 并非指向的内容
	Date* ptr1 = &d1;
	Date* ptr2 = &d2;
	cout << Less(ptr1, ptr2) << endl; // 可以比较 结果错误 
}

在这里插入图片描述

可以观察到:Less绝大多数情况都能正常比较,但是在特殊的场景下就会得到错误的结果。ptr1指向d1的地址明显小于ptr2指向d2的地址,但是d1 > d2,此时,就需要对模板进行特化,在原模板类的基础上,针对特殊类型所进行特殊化处理的方式

2.1. 函数模板的特化

  1. 必须现有一个基础的函数模板
  2. 关键template后面接一对空的尖括号<>(规定)
  3. 函数名后跟一对尖括号<>,尖括号中指定需要特化的类型
  4. 函数形参表:必须和函数模板的基础参数类型完全相同
// 特化指针比较
template<>
// const T& val 修饰的是这个值不可修改 
// 那么对应指针就是指向的内容不可修改 而不是指针不能修改
// 这个写法太怪了 函数模板特化不建议使用 需要特化的版本直接实现一个函数更好
bool Less<Date*>(Date* const& left, Date* const& right) { return *left < *right; }

bool Less(Date* left, Date* right) { return *left < *right; }
int main() {
	Date d1(2025, 10, 20);
	Date d2(2025, 10, 15);
	Date* ptr1 = &d1;
	Date* ptr2 = &d2;
	cout << Less(ptr1, ptr2) << endl; 
}

2.2.类模板的特化

全特化

全特化是把所有模板参数都具体成确定的实参

// 2.2. 全特化
// 类模板
template <class T1, class T2>
class Show {
public:
	Show() { cout << "Show<T1,T2>"<<endl; }
private:
	T1 _showInt;
	T2 _showChar;
};
// 全特化
template<>
class Show<int, char> {
public:
	Show() { cout << "Show<int,char>"<<endl; }
private:
	int _showInt;
	char _showChar;
};

int main() {
	Show<int, int> s1;
	Show<int, char> s2;
}

偏特化

只对一类形状定制实现——比如“第二个参数固定为 int “、“两个参数都是指针”、“T 的 const 版本”等。它仍然是模板,只是更具体

比如针对下面这个模板:

// 类模板
template <class T1, class T2>
class Show {
public:
	Show() { cout << "Show<T1,T2>"<<endl; }
private:
	T1 _showInt;
	T2 _showChar;
};
按“某个参数固定”特化
// 1>将第一个参数特化为int
template <int,class T2>
class Show {
public:
	Show() { cout << "Show<int,T2>" << endl; }
private:
	int _showInt;
	T2 _showChar;
};
按”两个参数同种类型“特化
// 2>  按”两个参数同种类型“特化 两个参数特化为指针类型
template <typename T1, typename T2>
class Show<T1*,T2*> {
public:
	Show() { cout << "Show<T1*,T2*>" << endl; }
private:
	T1* _showInt;
	T2* _showChar;
};
完整结果

最后,编译器会选择最合适的模板

// 2.3. 偏特化
// 1>将第一个参数特化为int
template <class T2>
class Show<int,T2> {
public:
	Show() { cout << "Show<int,T2>" << endl; }
private:
	int _showInt;
	T2 _showChar;
};

// 2>  按两个参数同种类型特化 两个参数特化为指针类型
template <typename T1, typename T2>
class Show<T1*,T2*> {
public:
	Show() { cout << "Show<T1*,T2*>" << endl; }
private:
	T1* _showInt;
	T2* _showChar;
};

int main() {
	Show<int, double> s1;
	Show<char, double> s2;
	Show<int*, double*> s3;

	return 0;
}

3. 什么时候用class 什么时候用typename ?

3.1.在模板参数列表里:class≈ typename(几乎等价)

表示“类型形参”时,两者可以互换,用哪个都行(团队统一即可)。

// 这两句等价
template <class T>    struct Box { T v; };
template <typename T> struct Bag { T v; };

Box<int> b{1};
Bag<int> g{2};

常见风格

  • typename读起来更明确“这是类型”。
  • 在“模板的模板参数”处,常见写法是:template <template<class...> class C, class T> struct W;

3.2. 在依赖名前:必须用 typename

当一个名字依赖模板参数,编译器仅凭语法不知道它是“类型”还是“值/成员”,这时要用 typename 告诉编译器它是类型。

#include <vector>
template <class T>
void printVector(const vector<T>& data) {
	typename vector<T>::const_iterator it = data.begin();
	while (it != data.end()) {
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

int main() {
	vector<int> arr1 = { 1, 2, 3, 4, 5 };
	vector<double> arr2 = { 1.1, 2.2, 3.3, 4.4, 5.5 };
	printVector(arr1);
	printVector(arr2);
}

如果不加typename,后果:

在这里插入图片描述

原因是类模板vector<T>并没有实例化,而编译器不会详细检查它是常量还是类型,所以添加typename是明确告诉编译器,这是一个类型

3.3. 调用依赖成员模板时:需要 template(不是 typename)

当在依赖对象上调用成员模板,要用 template 告诉编译器“后面的名字是模板,而不是普通成员”。

template <class T>
struct Has {
    template <class U>
    void bar(U) {}
};

template <class X>
void call(Has<X>& h) {
    //  这里要写 template,表示 bar 是成员模板
    h.template bar<int>(42);
}

这与 typename 不同:

  • typename:标注“这是类型”。
  • template:标注“这是模板”(用于依赖场景下的调用/取地址)。

4. 模板分离编译

4.1. 为什么“模板分离编译”会难?

模板的实例化发生在编译期,编译器在需要用到具体实参类型时才生成代码。
因此编译器在实例化点就必须看到模板的完整定义(不仅是声明),否则就会在链接阶段出现 undefined reference

  • 普通函数/类:可以把声明放头文件、定义放 .cpp,编译器不需要在调用点看到实现。
  • 模板:必须在实例化点拥有定义 → 常见策略就是把实现也放头文件

4.2.模板分离编译的两种主流做法

做法 A:头文件包含实现(最常见)

  • 方式:将声明和定义放到一个文件 xxx.hpp 里面或者xxx.h

  • 优点:简单直接、不会丢实例化、链接不踩坑。

  • 缺点:编译慢、潜在代码膨胀。

    文件结构:

    proj/
      ├─ max.hpp
      └─ main.cpp
    

    max.hpp

    #pragma once
    
    template <typename T>
    T my_max(T a, T b) {   // 模板“定义”也在头文件
        return a > b ? a : b;
    }
    

    main.cpp

    #include <iostream>
    #include "max.hpp"
    
    int main() {
        std::cout << my_max(3, 5) << "\n";        // int
        std::cout << my_max(2.5, 1.2) << "\n";    // double
    }
    

做法 B:显式实例化

  • 思路:库作者在某个 .cpp 里统一生成若干常用模板实参的目标代码,使用者只需链接该库而不必重新实例化。
  • 两步
    1. 头文件中仅给出模板声明
    2. 在一个 .cpp#include 头文件并写 template class Foo<int>; 这类显式实例化定义

文件结构:

proj/
  ├─ sum.hpp
  ├─ sum.cpp
  └─ main.cpp

sum.hpp

#pragma once

template <typename T>
T sum(const T* p, int n);

sum.cpp(定义 + 只在这里生成实例)

#include "sum.hpp"

template <typename T>
T sum(const T* p, int n) {
    T s{}; 
    for (int i = 0; i < n; ++i) s += p[i];
    return s;
}

// 这里显式实例化定义,真正产出代码
template int sum<int>(const int*, int);
template double sum<double>(const double*, int);

main.cpp(使用者只需要头文件 + 链接库目标)

#include <iostream>
#include "sum.hpp"

int main() {
    int ai[] = {1,2,3};
    double ad[] = {0.5, 1.5};
    std::cout << sum(ai, 3) << "\n";   // 用到 int 版本
    std::cout << sum(ad, 2) << "\n";   // 用到 double 版本
}
  • 优点:能做真正的“分离编译”,把模板库编成二进制发给别人;缩短使用者的编译时间。
  • 缺点:只对列出的类型生效,新类型用到时仍需要看到定义或新增实例化项;维护成本较高。

5.总结

  • 两类形参

    • 类型形参typename/class T)决定“长什么样”。
    • 非类型形参(如 size_t N,C++20 起可用更多字面量类型)决定“有多大/多少份”,常见于 std::array<T, N>、固定缓冲区等,类型中就“带着”数值。
  • 特化

    • 函数模板特化语义复杂、可读性一般;很多场景更建议直接重载
    • 类模板特化全特化(把形参全定死)与偏特化(对一类形状:某参数固定、指针版本、const 版本等)。编译器会选最匹配的那个。
  • class vs typename

    • 模板参数列表里几乎等价。
    • 依赖名前必须用 typename 表示“这是类型”。
    • 依赖对象调用成员模板时要用 templateobj.template foo<int>()
  • 分离编译

    • 做法 A(头文件包含实现):最稳妥,简单、少踩坑。
    • 做法 B(显式实例化):在 .cpp 里集中生成指定实参(template class/func …;),适合发布二进制与加速编译;但只覆盖列出的类型
    • 典型报错 undefined reference 往往是实例化点看不到定义或忘了提供显式实例化定义。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值