模板进阶
在之前我们学习过模板的基本使用,不熟悉的小伙伴可以翻看:C++(七)模板
在这里我们就直接更深入的学习模板
一、非类型模板参数
首先我们明确一点:模板分为类型模板和非类型模板
所以参数也分别对应为类型模板参数和非类型模板参数
1. 类型模板(Type Template)
类型模板是最常见的模板类型,它允许你为类或函数定义参数化的类型。这种模板参数代表一种数据类型,编译时会根据你提供的实际类型进行实例化。
示例:
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int x = 5, y = 10;
double a = 2.5, b = 4.5;
// 使用类型模板实例化函数(显式) 隐式也没有问题,为了易于观察写作显式
int int_result = add<int>(x, y); // int add(int, int)
double double_result = add<double>(a, b); // double add(double, double)
return 0;
}
在上面的例子中,add
函数是一个类型模板,T
是一个类型参数。这个模板可以处理任何数据类型,如int
或double
,而无需为每种类型编写不同的函数(交给编译器去做)。
在这里面 T就是
类型模板参数 ,T会自动识别(或指定)成int或者double等类型。
这就是一个最为简单直观的类型模板。
2. 非类型模板(Non-Type Template)
非类型模板的参数不是类型,而是常量表达式(如整数、指针、引用等)。这些参数在模板实例化时被指定,并且通常用来影响模板内部的实现。
非类型模板的参数就为: 非类型模板参数
什么意思呢?我们用一个最简单的例子看看非类型模板的使用:
template <int N>
int multiply(int a) {
return a * N;
}
int main() {
int result1 = multiply<5>(2); // 5 * 2 = 10
int result2 = multiply<10>(3); // 10 * 3 = 30
return 0;
}
在这个例子中,multiply
函数模板有一个非类型模板参数N
,它是一个整型常量。在实例化时,N
的值被替换为实际的常量值,如5
或10
。
注意:
- 非类型模板参数都被定义为常量表达式(const expression)这意味着它们的值在编译时是已知的。
-
常见的非类型模板参数类型包括:
- 整型常量 (
int
,char
,bool
, 等) - 枚举类型 (
enum
) - 指针类型 (指向对象或函数的指针)
- 引用类型 (对象的引用)
- 浮点数常量 (C++20及更高版本,现在大部分的编译器是不支持浮点型的!!!)
- 类的静态成员
- 整型常量 (
在这里主要介绍整型常量,使用相对比较多:
1.1.1整形常量
示例:
#include<iostream>
template <int N = 10>
int multiply(int a) {
return a * N;
}
int main() {
int result1 = multiply<5>(2); // 5 * 2 = 10
int result2 = multiply(3); // 10 * 3 = 30
std::cout << result1 <<std::endl;
std::cout << result2;
return 0;
}
程序运行结果:
30
10
在这段程序里面,N可以给上缺省值,这也是没有问题的。
只需要,且非常重要的一点:编译时常量,非类型模板参数必须是编译时常量,这意味着它们的值在编译时已确定。这里的N一定是一个常量且在编译时已确定,在函数中不可被修改!!!
错误用法:
template <int N = 10>
int multiply(int a) {
N=30;
return a * N;
}
int main() {
int result1 = multiply<5>(2); // 5 * 2 = 10
int result2 = multiply(3); // 10 * 3 = 30
return 0;
}
程序运行结果:
下面的几种用法并非很常用,可适当时候翻看:
1.1.2.枚举类型
示例:
#include<iostream>
enum Color {
Red,
Green,
Blue
};
template <Color C>
class ColorPrinter {
public:
void print() const {
//关键字:constexpr,用于表示一个表达式或函数可以在编译时求值
/* 如果 C 是 Red,则只会编译输出 "Red\n" 的代码块,其他分支不会编译。
* 如果 C 是 Green,则只会编译输出 "Green\n" 的代码块。
* 如果 C 是 Blue,则只会编译输出 "Blue\n" 的代码块。
*/
if constexpr (C == Red)
{
std::cout << "Red\n";
}
else if (C == Green)
{
std::cout << "Green\n";
}
else if (C == Blue)
{
std::cout << "Blue\n";
}
}
};
int main() {
ColorPrinter<Red> redPrinter;
redPrinter.print(); // 输出 "Red"
return 0;
}
代码运行结果:
Red
1.1.3 指针和引用
指针和引用类型的非类型模板参数用于传递对象的地址或引用。
示例:
#include<iostream>
template <int* ptr>
class PointerWrapper {
public:
void print() const
{
std::cout << "Value: " << *ptr << "\n";
}
};
int x = 5;
int main() {
PointerWrapper<&x> wrapper;
wrapper.print(); // 输出 "Value: 5"
return 0;
}
运行结果:
Value: 5
引用版本:
#include <iostream>
template <int& ref> // 使用引用作为非类型模板参数
class PointerWrapper {
public:
void print() const {
std::cout << "Value: " << ref << "\n"; // 直接输出引用的值
}
};
int x = 5;
int main() {
PointerWrapper<x> wrapper; // 使用引用的非类型模板参数
wrapper.print(); // 输出 "Value: 5"
return 0;
}
运行结果:
Value: 5
1.1.4 类的静态成员
因为现在很多编译器尚未支持浮点数常量,所以这里直接跳过了。
下面直接演示静态成员:
#include<iostream>
class MyClass {
public:
// 声明静态常量成员变量
static const int value ;
};
// 类外定义静态常量成员变量
const int MyClass::value = 42;
template <int N>
class StaticValuePrinter {
public:
void print() const
{
std::cout << "Value: " << N << "\n";
}
};
int main() {
StaticValuePrinter<MyClass::value> printer;
printer.print(); // 输出 "Value: 42"
return 0;
}
运行结果:
Value: 42
二、类模板特化
- 什么是类模板特化?
- 模板特化有什么用?
我们的学习围绕上述问题进行展开:
1.模板特化的定义
类模板特化(Class Template Specialization)是C++模板编程中的一个重要特性,允许针对特定的模板参数提供专门化的实现。
在我们学习的模板的过程中,分为类模板和函数模板,函数特化很自然的也分为
- 类模板特化
- 函数模板特化
2.类模板特化:
那模板特化到底什么意思?看以下代码:
#include <iostream>
// 通用的类模板
template <typename T>
class MyClass {
public:
void print() const
{
std::cout << "Generic template\n";
}
};
// 对int类型的特化 (可以看作为模板开的特殊通道,只要是int就走这边)
template <>
class MyClass<int> {
public:
void print() const
{
std::cout << "Specialized template for int\n";
}
};
int main() {
MyClass<double> obj1; // 使用通用模板
obj1.print(); // 输出: Generic template
MyClass<int> obj2; // 使用模板特化
obj2.print(); // 输出: Specialized template for int
return 0;
}
- 通用模板
MyClass<T>
提供了一个print
方法,适用于任何类型的T
。 MyClass<int>
是MyClass
的完全特化版本,专门针对int
类型,并提供了不同的print
方法。(可以看作int的特殊通道,只要是int就走这里)
仔细阅读以上代码可以看出
类模板特化的写法
1.必须先定义一个基本的类模板
2.使用 template<>
声明完全特化的模板或者声明部分特化,在模板参数中指定部分条件。
3.为特定的模板参数提供不同的实现。
上面又提到了2个概念:
- 完全特化
- 部分特化
那么什么是完全特化?什么是部分特化呢?
说起来也简单:
- 完全特化(Full Specialization):为特定的模板参数提供完全不同的实现。
- 部分特化(Partial Specialization):为一部分模板参数提供特定的实现,而不是所有的模板参数。
上面我们举的第一个例子里面就是完全特化,就是将基础模板的全部参数都提供特定的实现,上面示例代码基础模板的全部参数就 T:将其特化成int
template <typename T>
完全特化:
template <>
class MyClass<int>
{
... ...
};
理解了上面内容之后我们再看看什么是部分特化:
首先一个示例:(完全特化)
#include <iostream>
// 通用的类模板
template <typename T1, typename T2>
class MyClass {
public:
void print() const {
std::cout << "基础 MyClass \n";
}
};
// 完全特化:针对类型 int 和 double 的组合
template <>
class MyClass<int, double>
{
public:
void print() const {
std::cout << "完全特化 <int double>\n";
}
};
int main() {
MyClass<int, double> obj1; // 使用完全特化
obj1.print(); // 输出
MyClass<int, int> obj2; // 使用通用模板
obj2.print(); // 输出
return 0;
}
运行结果:
完全特化 <int double>
基础 MyClass
观察以上代码,和第一个例子没有本质的不同只不过是类模板参数多了一个。
完全特化,MyClass<int, double>
针对 int
和 double
的组合提供了特定的实现。这种实现会完全替代通用模板,当模板参数是 int
和 double
时,编译器会选择这个特化版本(走这个特殊通道)。
基于上面代码,如果让它是一个部分特化应该怎么写呢?
示例(部分特化):
template <typename T1, typename T2>
class MyClass {
public:
void print() const {
std::cout << "基础 MyClass \n";
}
};
// 部分特化:当两个类型相同
template <typename T2>
class MyClass<int, T2> {
public:
void print() const {
std::cout << "部分特化 \n";
}
};
int main() {
MyClass<int, int> obj1; // 使用部分特化
obj1.print();
MyClass<double, double> obj2; // 使用基础模板
obj2.print();
return 0;
}
运行结果:
部分特化
基础 MyClass
部分特化:MyClass<int, T2>
部分特化了 T1
为 int
,T2
保持泛化。这意味着当 T1
是 int
时,不论 T2
是什么类型,都会使用这个部分特化的版本(T1是int 优先走这边)。
在部分特化中的写法是值得我们注意的
如果想要只特化T2,T1还是使用任意类型,语法也是支持的
// 部分特化:当第二个类型是 int 时
template <typename T1>
class MyClass<T1, int>
{
... ...
};
那么如果全特化和部分特化同时存在,那main函数里面的对象会走哪一个呢?
先说结论:哪个更匹配走哪一个!!!
书面来说模板特化的规则:
- 完全特化:最具体,针对一个完全确定的类型组合,如
MyClass<int, double>
。 - 部分特化:相对于通用模板来说更具体,但仍然可以匹配多种类型组合,如
MyClass<int, T2>
或MyClass<T, T>
。 - 通用模板:最不具体,适用于任何类型组合。
代码演示:(大家可以先思考下面代码obj1,obj2,obj3分别的输出是什么再看运行结果)
#include <iostream>
template <typename T1, typename T2>
class MyClass {
public:
void print() const {
std::cout << "基础 MyClass \n";
}
};
// 部分特化:当两个类型相同
template <typename T>
class MyClass<T, T> {
public:
void print() const {
std::cout << "部分特化 \n";
}
};
// 部分特化:当第一个类型是 int 时
template <typename T2>
class MyClass<int, T2> {
public:
void print() const {
std::cout << "部分特化 <int, T2>\n";
}
};
// 完全特化:针对类型 int 和 double 的组合
template <>
class MyClass<int, double>
{
public:
void print() const {
std::cout << "完全特化 <int double>\n";
}
};
int main() {
MyClass<int, double> obj1;
obj1.print();
MyClass<int, char> obj2;
obj2.print();
MyClass<short, double> obj3;
obj3.print();
return 0;
}
运行结果:
完全特化 <int double>
部分特化 <int, T2>
基础 MyClass
关于类模板特化这里就先展示这么多,小伙伴有疑问可以多多实践,多敲敲研究更多的变化。
3.函数模板特化
学习了上面类模板的使用,函数模板的使用也是如法炮制,只是在代码的书写使用中略微有点差别。
函数模板特化的使用步骤:
- 定义通用的函数模板(在特化之前必须先有一个基础的函数模板)
- template<> 声明和定义特化的模板函数
【注意】:函数模板特化不支持部分模板特化,只有全特化
示例:
#include <iostream>
#include <string>
// 1. 定义一个通用的函数模板
template <typename T>
void print(T value) {
std::cout << "基础: " << value << std::endl;
}
// 2. 特化函数模板(特化int类型)
template <>
void print<int>(int value) {
std::cout << "特化int类型: " << value << std::endl;
}
// 2. 特化函数模板(特化std::string类型)
template <>
void print<std::string>(std::string value) {
std::cout << "特化std::string类型: " << value << std::endl;
}
int main() {
// 3. 调用模板特化函数
print(10); // 调用特化的int版本
print(3.14); // 调用通用版本
print("Hello"); // 调用通用版本
print(std::string("Hello")); // 调用特化的std::string版本
return 0;
}
如果理解了类模板的特化,函数模板的特化应该还是很好理解的,知道大家需要注意代码实践写的时候,语法应该是这么写的。
关于模板进阶暂时就提及这么多下面个大家提供一个脑图,帮助大家更清晰的学习理解模板。