C++模板学习笔记(一)

记录一下学习C++模板时的知识点

两阶段模板编译检查

模板的检查分成两阶段,在定义时和在实例化时进行检查。在模板定义时检查:

  • 语法检查,比如分号。
  • 使用了未知的类型或函数,且与模板参数无关。

在模板实例化阶段,会将类型带入模板再次进行检查,比如以下代码:

1
2
3
4
5
6
7
8
template<typename T>
void foo(T t)
{
undeclared(); // 如果 undeclared()未定义,第一阶段就会报错,因为与模板参数无关
undeclared(t); //如果 undeclared(t)未定义,第二阶段会报错,因为与模板参数有关
static_assert(sizeof(int) > 10,"int too small"); // 与模板参数无关,总是报错
static_assert(sizeof(T) > 10, "T too small"); //与模板参数有关,只会在第二阶段报错
}

有些错误只会在模板实例化时出现,因此如果模板只是定义了,但是没有实例化过,这个错误可能一直存在但是不会被发现。

模板的定义

模板通常在声明时定义,但也支持声明与定义分离,例如在同一个头文件中

1
2
3
4
5
6
7
8
9
10
// MyClass.hpp
template <typename T>
class MyClass {
public:
void memberFunc();
};

// 定义必须写在同一个头文件内!
template <typename T>
void MyClass<T>::memberFunc() { /* ... */ }

处于编译效率考虑,也可以将定义放在其他头文件中,有些项目将包含有模板定义的文件添加在_impl.h后缀,供需要实例化的编译单元包含。

1
2
3
4
5
6
7
// MyClass.cpp
template <typename T>
void MyClass<T>::memberFunc() { /* ... */ }

// 显式实例化所需类型
template class MyClass<int>; // 实例化int版本
template class MyClass<double>; // 实例化double版本

将模板定义移到单独头文件的核心价值是 通过显式实例化控制代码生成,从而优化编译速度和封装性,但会牺牲模板的灵活性(用户无法随意指定新类型)。

模板的默认参数

一个有意思的地方,即使后面的模板参数没有指定默认指,依然可以让前面的模板参数有默认值

1
2
3
4
5
template<typename RT = long, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

模板特化

什么是模板特化

为特定类型的模板提供一个“特殊”版本,作为模板定义的一个补充。模板的特化可以分成全特化和偏特化,全特化即完全指定所有模板参数,偏特化则为指定部分模板参数。

1
2
3
4
5
6
7
8
9
10
11
// 定义一个模板函数
template<typename T>
bool equal(T a, T b) {
return a == b;
}

// 定义类型为double时的行为
template <>
bool equal(double a, double b) {
return abs(a-b) < 1e-5;
}

模板函数

实际上模板函数不存在模板偏特化,类似模板偏特化的功能是通过函数重载实现的。原因是函数重载可以实现模板特化的功能,就不再需要引入更加复杂的模板偏特化功能了。以下是利用函数重载实现模板偏特化的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T, typename U>
void foo(T a, U b) {
std::cout << "Generic: T=" << typeid(T).name() << ", U=" << typeid(U).name() << std::endl;
}

// 通过重载模拟"部分特化"
template <typename U>
void foo(int a, U b) { // 第一个参数固定为 int
std::cout << "Overload (T=int): U=" << typeid(U).name() << std::endl;
}

int main() {
foo(1, 2); // 调用重载版本 foo<int, int>(int, int)
foo(1, "hello"); // 调用重载版本 foo<int, const char*>(int, const char*)
foo(3.14, 'a'); // 调用主模板 foo<double, char>(double, char)
return 0;
}

模板类

模板的成员函数特化有多种方式。可以对整个模板类进行特化,特化时需要特化所有的成员函数,如果在特化时没有定义该成员函数,那么在后续使用时也不能使用该成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "bits/stdc++.h"

using namespace std;

template <typename T>
class MyClass {
public:
void func1() { cout << "Generic func1\n"; }
void func2() { cout << "Generic func2\n"; }
};

template <>
class MyClass<int> {
public:
void func1() { cout << "Specialized func1 for int\n"; }
// void func2() { cout << "Specialized func2 for int\n"; }
};

int main() {
MyClass<int> a;
a.func1(); // Output: Specialized func1 for int
a.func2(); // Error! func2 is not specialized for int
return 0;
}

上面这个例子特化了一个int类型版本的MyClass,在特化时没有特化func2函数,因此在a调用func2时会编译错误。特化后的模板类不会继承通用实现版本的成员函数。

除了对整个模板类进行特化,还允许对模板类的单个成员函数进行特化,这样就能保持特化时继承通用版本的成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <typeinfo>

template <typename T>
class Printer {
public:
void printType() {
std::cout << "Generic type: " << typeid(T).name() << "\n";
}

// 声明特化版本的成员函数
void printSpecial();
};

template <>
void Printer<int>::printSpecial() {
std::cout << "Specialized for int!\n";
}

// 其他类型仍然使用通用版本(未特化时可能未定义,需谨慎)
int main() {
Printer<double> p1;
p1.printType(); // 输出: Generic type: d (double)
// p1.printSpecial(); // 未定义,编译错误(未特化 double)

Printer<int> p2;
p2.printType(); // 输出: Generic type: i (int)
p2.printSpecial(); // 输出: Specialized for int!

return 0;
}

此时成员函数就与普通函数一致,遵循通用的规则,例如只能全特化而不支持偏特化。

模板别名

模板别名可以便于我们使用,有这么几种常用用法

  • typedef 关键字定义: typedef Stack<int> IntStack
  • using 关键字定义(C++11):using IntStack = Stack<int>

也可以alias封装出一个新模板,例如:

1
2
3
4
5
6
7
8
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;

// 链式封装
template<typename T>
using EntityWithLogAndTimer = Logger<Timer<T>>;

using MyBetterEntity = EntityWithLogAndTimer<BaseEntity>;
作者

echo

发布于

2025-07-14

更新于

2025-07-14

许可协议

评论