C++模板
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.
类模板的声明,定义和实例化
类模板可以用关键字 template
和 typename
来声明和定义。先看声明:
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]},但是将其分离至 hpp
和 cpp
没有什么必要。。。
函数模板
函数模板的声明,定义和调用
函数模板和类模板的语法相同。也是以关键字 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到任意个数、任意类型的参数。
语法是在 typename
或 class
关键字后加上 ...
,这种实现方式叫做参数包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=1
,args...=2.5, "hello", std::string("world")
第2次调用时,first=2.5
,args...="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;
}
模板全特化和偏特化
全特化
全特化的模板参数列表应当是空的。类模板,函数模板,变量模板都可以全特化。通俗的理解,全特化就是给特性类型“开小灶:,当模板对某个具体类型不适用或低效,可以专门为这个类型写一套实现。
- 模板函数的全特化
比如一个通用打印函数 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"”
- 类模板的全特化
通用的类模板相当于一个万能钥匙,能处理大多数情况,但不够灵活;全特化类模板就是给某个特殊类型做一把专用钥匙,能够实现一些专属功能。
// 通用模板:适合大多数类型
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(); }
};
- 变量模板的全特化
以上面的代码为例:
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,这涉及到了浮点到整形的隐式转换,小数点被直接截断了,属于被动转换,但是全特化的方式是主动的。
偏特化
模板偏特化给某一类类型(相比单个具体类型)“批量定制” 实现。
值得注意的是函数模板不允许偏特化。
- 类模板的偏特化
// 通用类模板:处理普通类型
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*
等)单独写全特化。
- 变量模板的偏特化
// 通用模板:大多数类型的默认容量是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
[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)评论