第七章:模板编程——让代码更通用
模板编程,说白了就是写一份代码,让它能处理多种数据类型。我刚开始学C++时,总觉得模板这玩意儿有点玄乎,直到有一次在项目中要写一个通用的排序函数,才真正体会到它的威力。
7.1 函数模板:一劳永逸的写法
想象一下,你要写一个求两个数最大值的函数。如果没有模板,你得为int、double、float各写一份。有了模板,一份就够了。
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 使用
int x = max(3, 5); // T被推导为int
double y = max(3.14, 2.7); // T被推导为double
我个人习惯把模板参数命名为T,简单明了。当然你也可以用U、V,或者更有意义的名字。
小技巧:如果你想让函数支持不同类型参数,可以这样写:
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(a > b ? a : b) {
return (a > b) ? a : b;
}
7.2 类模板:让类也通用起来
类模板和函数模板的思路一样,只是作用在类上。我在项目中经常用类模板来实现容器类,比如一个简单的栈:
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& elem) { elements.push_back(elem); }
T pop() {
T elem = elements.back();
elements.pop_back();
return elem;
}
bool empty() const { return elements.empty(); }
};
// 使用
Stack<int> intStack;
Stack<std::string> stringStack;
你想想看,如果没有模板,你得为每种类型写一个Stack类,那得多累啊。
7.3 模板特化与偏特化
有时候,通用模板不能满足所有情况。比如,对于指针类型,你可能想要特殊处理。这时候就需要模板特化。
全特化
全特化就是为某个特定类型写一个完全不同的实现。
// 通用模板
template <typename T>
class Printer {
public:
void print(const T& value) {
std::cout << "通用: " << value << std::endl;
}
};
// 全特化:针对const char*
template <>
class Printer<const char*> {
public:
void print(const char* value) {
std::cout << "字符串: " << value << std::endl;
}
};
偏特化
偏特化是部分特化,比如针对指针类型,但不管具体是什么指针。
// 通用模板
template <typename T>
class MyClass {
public:
void info() { std::cout << "通用模板" << std::endl; }
};
// 偏特化:针对指针类型
template <typename T>
class MyClass<T*> {
public:
void info() { std::cout << "指针特化" << std::endl; }
};
// 使用
MyClass<int> obj1; // 调用通用模板
MyClass<int*> obj2; // 调用偏特化版本
我曾经踩过的坑:偏特化只能用于类模板,不能用于函数模板。如果你需要对函数模板做特化,只能用全特化或者重载。
7.4 模板参数推导
C++17之后,模板参数推导变得更智能了。你甚至不需要显式指定模板参数:
std::pair p(1, 3.14); // C++17自动推导为pair<int, double>
std::vector v = {1, 2, 3}; // 推导为vector<int>
但要注意,不是所有情况都能推导成功。比如:
template <typename T>
void func(T a, T b) {}
func(1, 3.14); // 错误:T被推导为int和double,冲突了
这时候你需要显式指定:func<double>(1, 3.14);
7.5 模板与继承的结合
模板和继承结合起来,能产生非常强大的效果。我在项目中用过一种叫CRTP(奇异递归模板模式)的技巧:
// 基类模板
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
// 派生类
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived实现" << std::endl;
}
};
这种模式的好处是,你可以在基类中定义通用接口,而具体实现由派生类提供。说白了,就是一种编译期的多态。
知识体系总览
下面这张图展示了模板编程的核心知识点和它们之间的关系:
避坑指南
我曾经犯过的错误:
- 模板代码写在.cpp文件中,导致链接错误。记住:模板的声明和定义必须放在头文件里。
- 模板参数推导失败时,编译器报错信息又长又难懂。我的建议是:先检查类型是否匹配,再检查是否有歧义。
- 滥用模板会让编译时间变长。我一般只在确实需要通用性的地方才用模板。
模板编程是C++中非常强大的特性,但也要适度使用。我个人觉得,掌握好函数模板和类模板就够应付大部分场景了。特化和偏特化属于进阶技巧,遇到具体需求时再学也不迟。