第七章:模板编程——让代码更通用

模板编程,说白了就是写一份代码,让它能处理多种数据类型。我刚开始学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;
    }
};

这种模式的好处是,你可以在基类中定义通用接口,而具体实现由派生类提供。说白了,就是一种编译期的多态。

知识体系总览

下面这张图展示了模板编程的核心知识点和它们之间的关系:

模板编程 函数模板 类模板 特化与偏特化 template<typename T> T max(T a, T b) 自动类型推导 template<typename T> class Stack 成员函数模板 全特化:template<> class Printer<char*> 偏特化:template<T> class MyClass<T*> 模板与继承结合(CRTP)

避坑指南

我曾经犯过的错误:

  • 模板代码写在.cpp文件中,导致链接错误。记住:模板的声明和定义必须放在头文件里。
  • 模板参数推导失败时,编译器报错信息又长又难懂。我的建议是:先检查类型是否匹配,再检查是否有歧义。
  • 滥用模板会让编译时间变长。我一般只在确实需要通用性的地方才用模板。

模板编程是C++中非常强大的特性,但也要适度使用。我个人觉得,掌握好函数模板和类模板就够应付大部分场景了。特化和偏特化属于进阶技巧,遇到具体需求时再学也不迟。

公众号:蓝海资料掘金营,微信deep3321