Skip to content

Chapter 10 Templates

Why templates?

如果你需要一个 X 类型的链表和一个 Y 类型的链表,二者代码逻辑很相似,差别只在于元素类型,那么传统的选择是:

  • 复制多份代码:保留类型安全,但难维护
  • 提炼公共基类:并不总是合适
  • 无类型容器:类型不安全

而模板的核心是用类型作为参数,以泛型编程(generic programming)的方式复用源代码。

1 Basic

  • 函数模板(Function Templates):用一份代码处理多种类型上的相似操作
示例
普通 swap
void swap(int& x, int& y) {
    int temp = x;
    x = y;
    y = temp;
}
模板版 swap
template <class T>
void swap(T& x, T& y) {
    T temp = x;
    x = y;
    y = temp;
}
  • template 关键字引入模板,class T 表示参数化的类型名
  • 这里的 class 并不限于类类型,也可以是内置类型
  • 模板实例化(Template Instantiation):编译器根据模板实参替换模板中的类型参数生成新的函数/类定义,随后再进行语法和类型检查,只有真正被使用到的模板版本才会被实例化出来
示例
int i = 3, j = 4;
swap(i, j); // 实例化 int 版本

float k = 4.5, m = 3.7;
swap(k, m); // 实例化 float 版本

std::string s("Hello"), t("World");
swap(s, t); // 实例化 std::string 版本

模板参数要求精确匹配,不做类型转换,即使隐式转换通常也会被忽略。

示例
swap(int, int);       // ok
swap(double, double); // ok
swap(int, double);    // error

普通函数与模板函数共存时,先找唯一的普通函数精确匹配,再找唯一的模板函数匹配,最后才考虑普通函数的隐式转换。

示例
void f(float i, float k) {}

template <class T>
void f(T t, T u) {}

f(1.0f, 2.0f);    // 普通函数
f(1.0, 2.0);      // 模板函数
f(1, 2);          // 模板函数 
f(1, 2.0);        // 普通函数

一般编译器会从实参中推导类型,若无法推导,也可以手动显式指定。

示例
template <class T>
void foo() { /* ... */ }

foo<int>();    // type T is int
foo<float>();  // type T is float

2 Class Templates

  • 类模板(Class Templates):用类型参数化类,常见于容器类,能定义出潜在无限多个不同类
示例:Vector 类模板
template <class T>
class Vector {
public:
    Vector(int);
    ~Vector();
    Vector(const Vector&);
    Vector& operator=(const Vector&);
    T& operator[](int);
private:
    T* m_elements;
    int m_size;
};

Vector<int> v1(100);
Vector<Complex> v2(256);

v1[20] = 10;
v2[20] = v1[20]; // 若支持 int -> Complex 转换则合法
示例:成员定义
template <class T>
Vector<T>::Vector(int size) : m_size(size) {
    m_elements = new T[m_size];
}

template <class T>
T& Vector<T>::operator[](int index) {
    if (index < m_size && index >= 0) {
        return m_elements[index];
    } else {
        // ...
    }
}
示例:泛型冒泡排序
template <class T>
void sort(Vector<T>& arr) {
    const size_t last = arr.size() - 1;
    for (int i = 0; i < last; i++) {
        for (int j = last; j > i; j--) {
            if (arr[j] < arr[j - 1]) {
                swap(arr[j], arr[j - 1]);
            }
        }
    }
}

Vector<int> vi(4);
vi[0] = 4; vi[1] = 3; vi[2] = 7; vi[3] = 1;
sort(vi);  // 调用 sort(Vector<int>&)

Vector<string> vs(5);
vs[0] = "Fred"; vs[1] = "Wilma"; vs[2] = "Barney"; vs[3] = "Dino"; vs[4] = "Prince";
sort(vs);  // 调用 sort(Vector<string>&)

模板算法常依赖元素类型提供相应操作,如这里要求 T 支持 operator<

3 Others

  • 模板支持多个类型参数。
示例
template <class Key, class Value>
class HashTable {
    const Value& lookup(const Key&) const;
    void insert(const Key&, const Value&);
};
  • 模板可以嵌套(Nesting Templates),其类型参数可以很复杂。
示例
Vector<Vector<double*> >
Vector<int (*)(Vector<double>&, int)>
  • 模板参数不一定是类型,也可以是常量表达式,可以带默认值。
    • 优点:可能更快。
    • 缺点:增加复杂度,类型上带着尺寸信息,可能导致代码膨胀(code bloat)
示例
非类型模板参数
template <class T, int bounds = 100>
class FixedVector {
public:
    FixedVector();
    T& operator[](int);
private:
    T elements[bounds];    // fixed-size array!
};
成员定义与使用
template <class T, int bounds>
T& FixedVector<T, bounds>::operator[](int i) {
    return elements[i];
}

FixedVector<int, 50> v1;
FixedVector<int, 10 * 5> v2;
FixedVector<int> v3; // 使用默认值 100
  • 模板类可继承普通类。
示例
template <class A>
class Derived : public Base { /* ... */ };
  • 模板类也可继承模板类。
示例
template <class A>
class Derived : public List<A> { /* ... */ };
  • 普通类也可继承模板实例。
示例
class SupervisorGroup : public List<Employee*> { /* ... */ };
CRTP: Curiously Recurring Template Pattern
template <class T>
class Base { /* ... */ };

class Derived : public Base<Derived> { /* ... */ };

静态多态

template <class T>
class Base {
public:
    void interface() {
        static_cast<T*>(this)->implementation();
    }
    static void static_func() {
        T::static_sub_func();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation();
    static void static_sub_func();
};

模板的声明和定义通常都放在头文件里,因为实例化发生在使用点,编译器必须能看到完整定义,若只把定义放进 .cpp,其他翻译单元通常无法完成实例化。模板还能配合 friendstatic members

Writing Templates Workflow
  1. 先写一个非模板版本并跑通
  2. 建立一套可靠测试用例
  3. 测性能并调优
  4. 回看实现,确认哪些类型应参数化
  5. 再改写成模板
  6. 用既有测试再次验证