C++ 模板与泛型编程
1.泛型编程 简介
1.1 定义
C++ 中的泛型编程是一个强大的技术,可以用来编写高效、可扩展且通用的代码。它的核心机制是模板(Templates),泛型编程是一种使用模板来编写代码的方法,可以让程序员编写与具体数据类型无关的代码,通过在编译时生成代码实例来实现类型无关的设计;
1.2 诞生
泛型编程最初诞生于C++中,由Alexander Stepanov[2]和David Musser[3]创立。目的是为了实现C++的STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。
把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。比如qsort泛化之后就变成了:
template<class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first, RandomAccessIterator last,
Compare comp);
1.3泛型编程样例
通用的加法函数:
// 参数 int
int Add(int& x, int& y) { return x + y; }
/ 参数 double
double Add(double& x, double& y) { return x + y; }
/// 参数 自定义类型
AType Add(AType& x, AType& y) { return x + y; }
使用函数重载虽然可以实现,但是有以下几个不好的地方:
A. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
B. 代码的可维护性比较低,一个出错可能所有的重载均出错。
这个时候,我们可以使用模板函数, 编译器根据不同的类型,利用该模子来自己生成代码;
template <class T>
T add(T num1, T num2)
{
return num1+num2;
}
这种编程思想就是 泛型编程 :一种编程范式,编写与类型无关的通用代码,是代码复用的一种手段;模板是泛型编程的基础,包含函数模板和类模板;
2、模板(Template)
模板是 C++ 泛型编程的基础,分为函数模板和类模板。
2.1 函数模板(function template)
函数模板允许编写与类型无关的函数。通过定义模板参数,函数可以应用于多种类型的数据
template <typename T>
T add(T a, T b) {
return a + b;
}
模板定义以关键字template开始,后接尖括号括住的模板形参表。
模板形参可以是表示类型的类型形参(type parameter),也可以是表示常量表达式的非类型形参(nontype parameter)。上面程序中的T是类型形参。
int main() {
std::cout << add(1, 2) << std::endl; // 输出 3
std::cout << add(1.1, 2.2) << std::endl; // 输出 3.3
}
使用函数模板时,编译器会将模板实参绑定到模板形参。编译器将确定用什么类型代替每个类型形参,用什么值代替每个非类型形参,然后产生并编译(称为实例化)该版本的函数。
上面的例子中,编译器用int代替T创建第一个版本,用double代替T创建第二个版本。
函数模板也可以声明为inline
// inlinespecifier follows template parameter list
template<typename T> inline T add(const T&, const T&);
2.2 类模板 (class template)
类模板用于创建具有类型参数的类,提供更灵活的数据结构和算法实现;
在定义的类模板中,使用模板形参作为类型或值的占位符,在使用类时再提供具体的类型或值。
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& element) {
elements.push_back(element);
}
T pop() {
T top = elements.back();
elements.pop_back();
return top;
}
};
与调用函数模板不同,使用类模板时,必须为模板形参显示指定实参。
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << intStack.pop() << std::endl; // 输出 20
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << stringStack.pop() << std::endl; // 输出 "World"
}
2. 3 模板特化
模板特化允许为特定的模板参数类型定义特殊行为
2.3.1 完全特化
完全特化为特定类型提供定制实现。
template <>
class Stack<bool> {
private:
std::vector<uint8_t> elements;
public:
void push(bool value) {
elements.push_back(value ? 1 : 0);
}
bool pop() {
uint8_t value = elements.back();
elements.pop_back();
return value;
}
};
2.3.2 偏特化
偏特化允许部分模板参数被固定,剩余的参数仍然可以是泛型的
template <typename T1, typename T2>
class Pair;
template <typename T>
class Pair<T, int> {
// T 可以是任意类型,但第二个参数固定为 int
};
3. 模板元编程(Template Metaprogramming)
3.1递归模板
用递归模板计算阶乘:
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
int main() {
std::cout << Factorial<5>::value << std::endl; // 输出 120
}
3.2 类型特性与 SFINAE
SFINAE(Substitution Failure Is Not An Error)是模板编程中重要的概念,用于启用/禁用模板的特定实现
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
square(T value) {
return value * value;
}
int main() {
std::cout << square(5) << std::endl; // 输出 25
// std::cout << square(5.5) << std::endl; // 编译错误
}
4. Concepts(C++20 新特性)
Concepts 提供了一种显式约束模板参数的方法,增强代码的可读性和错误提示
#include <concepts>
template <std::integral T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << std::endl; // 输出 3
// std::cout << add(1.1, 2.2) << std::endl; // 编译错误
}
5.实践建议
- 选择合适的模板类型
- 使用
typename
和class
定义模板参数没有功能上的差异,选用风格一致的关键词。
- 使用
- 合理使用特化与 SFINAE
- 不要过度复杂化模板逻辑,避免代码难以维护。
- 结合 Concepts
- 对模板参数设置合理的约束,提高错误信息的可读性。
- 优化编译性能
- 泛型编程会生成大量代码实例,注意模块化设计,减少重复实例化。
6.小结
C++ 泛型编程提供了极大的灵活性和功能,但也对代码设计能力和理解能力提出了更高的要求。结合实际需求和新的语言特性,可以写出高效、优雅的泛型代码。