泛型编程 
前面我们提过,宏定义尽管很方便,效率比较高。但由于它对语言本身不甚理解,会导致代码可读性太差,并且很难调试。因此,在 C++ 中引入了泛型编程的思想,并增加了一系列工具。这些工具提供了比文本替换更强大且更智能的操作,方便我们进行泛型程序设计。
模板 
模板是 C++ 中的一种泛型编程工具,它允许我们编写通用代码,以便在不同的数据类型上使用。模板的基本思想是将类型参数化,使得我们可以在编译前指定类型,并由编译器自动生成实际代码。
模版的定义为 template <typename T>,其中 typename 是关键字,T 是类型参数。我们可以在函数、类、结构体中使用模板的类型参数,以实现泛型编程。
函数模板 
函数模版是通用的函数描述,使用泛型来定义函数。我们可以像普通函数一样调用函数模版,在通常情况下,编译器会为我们补全类型相关的信息。
template <typename T>
void swap(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}
int main() {
    int a = 1, b = 2;
    swap(a, b);
    std::cout << a << " " << b; // 输出为 “2 1”
    return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
如果我们希望有时避免编译器自动推断类型,或是指定在某些特定类型上模版的小变化,我们可以使用显式具体化的技术。
template <typename T>
void swap(T &, T &); // 原始模版函数
template <> void swap<Job>(Job &, Job &); // 显式具体化2
3
4
当我们传入 swap 函数的参数是 Job 类型时,编译器会优先选择显式具体化的函数,否则,编译器会默认选择原始模版函数。
类模板 
类模版和函数模版类似,可以用来生成通用的类声明。同样,我们需要在类声明之前加上 template <typename T>,并在类中使用 T 作为希望被替换的类型参数。
下面的 ArrayList.h 定义了一个简单的可以自动扩容的数组类,我们可以在其中存储任意类型的数据。
ArrayList.h 的代码实现
#ifndef ARRAYLIST_H
#define ARRAYLIST_H
#include <optional>
#include <cstring>
#include <iostream>
#include <bits/stdc++.h>
template<typename T>
struct ArrayList {
    T *array;
    int capacity; // allocated size
    int size; // actual element occupation 
    int elem_size; // size of each element
    ArrayList() {
        size = 0;
        capacity = 10;
        elem_size = sizeof(T);
        array = static_cast<T *>(malloc(capacity * elem_size));
    }
    void add(T x) {
        if (size == capacity) {
            capacity = capacity + (capacity >> 1);
            array = static_cast<T *>(realloc(array, capacity * elem_size));
        }
        array[size] = x;
        size++;
    }
    void remove(T x) {
        int pos = -1;
        for (int i = 0; i < size; i++) {
            if (array[i] == x) {
                pos = i;
                break;
            }
        }
        if (pos == -1) return;
        for (int i = pos; i < size - 1; i++) {
            array[i] = array[i + 1];
        }
        size--;
    }
    std::optional<T> get(int index) {
        if (index >= size || index < 0) {
            return std::nullopt;
        }
        return array[index];
    }
    int getSize() {
        return size;
    }
    int getCapacity() {
        return capacity;
    }
};
#endif // ARRAYLIST_H2
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
在创建类(实例化)时,我们可以指定 ArrayList 类的类型参数,以实现泛型编程。例如,我们要新建一个元素类型为 int 的 ArrayList 对象并对其简单操作,可以这样写:
ArrayList<int> list;
list.add(1);
list.add(2);
std::cout << list.get(0).value() << std::endl; // 输出为 “1”2
3
4
Concept C++20 
C++20 引入了 Concept 的概念,它是对模板的一种约束,可以限制模板的类型参数。这一限制可以提高代码的可读性,加快编译速度,并生成更易懂的错误信息。例如,有些时候我们希望模版类型需要满足可加性,这时我们可以使用 Concept 来约束模版类型。
显然,在前文的 ArrayList 的例子中,模版的类型参数应当可以按值拷贝并比较相等性。 因此,我们对前文的 ArrayList 进行修改,在 template<typename T> 后面添加一个 Concept 约束。
concept ArrayListCompatible = 
    std::is_copy_assignable_v<T> && 
    requires(T a, T b) {
        { a == b } -> std::convertible_to<bool>;
    };
    
template<typename T> requires ArrayListCompatible<T>2
3
4
5
6
7
这时,当我们传入一个自定义的类型作为参数,如果该类型不满足 ArrayListCompatible 的约束,编译器会报错。例如
struct Job {
    int id;
    std::string name;
};2
3
4
当传入 Job 类型时,可以编译通过;当传入 const Job 类型时,编译器会报错。
Concept 的语法
在声明类模版的 Concept 时,我们可以先定义一个 Concept,然后在模版声明时使用 requires 关键字来约束模版的类型参数。一般的范式如下:
template<typename T>
concept ConceptName = 
    // 约束条件, 可以用逻辑运算符连接多个约束条件
    requires(T a, T b) {
        // 约束条件
    };
template<typename T> requires ConceptName<T>
// 类模版声明2
3
4
5
6
7
8
9
template<typename T> requires ConceptName<T> 还可以简写为 template<ConceptName T>。
当声明函数模版时,与之类似,我们只需在声明函数时使用 requires 关键字即可,如
template<typename T>
void func(T a) requires ConceptName<T> {
    // 函数体
}2
3
4
Concept 的约束条件
至于约束条件,可以在 CPPReference 上的相关页面查看,下面列出一些常用的约束条件:
| Concept 名称 | 定义 | 典型应用场景 | 
|---|---|---|
| std::same_as<T, U> | T和U是完全相同的类型(包括 const/volatile 修饰符) | 确保模板参数是特定类型,例如:检查两个参数类型相同 | 
| std::integral<T> | T是一个整数类型(如int,long) | 适用于整数运算,整数专用函数 | 
| std::floating_point<T> | T是浮点数类型(如float,double) | 限制模板参数为浮点数,应用于计算浮点数函数 | 
| std::signed_integral<T> | T是有符号整数类型(如int,long) | 有符号整数运算 | 
| std::unsigned_integral<T> | T是无符号整数类型(如unsigned int,unsigned long) | 限制模板为无符号整数,应用于二进制运算 | 
| std::default_initializable<T> | T可以用默认构造函数构造 | 用于需要默认构造对象的容器或数据结构 | 
| std::destructible<T> | T类型是可销毁的(具有析构函数) | 需要动态创建和销毁对象的情况 | 
| std::constructible_from<T, Args...> | T类型可以用Args...构造参数构造 | 限制模板类型可以用特定参数构造 | 
| std::assignable_from<T, U> | T可以从U进行赋值操作 | 用于对象赋值,例如容器的赋值操作 | 
| std::copy_constructible<T> | T类型支持拷贝构造函数 | 需要复制对象的容器、数据结构 | 
| std::move_constructible<T> | T类型支持移动构造函数 | 适用于移动语义,减少不必要的拷贝 | 
| std::swappable<T> | T类型支持std::swap或自定义的swap函数 | 在需要交换元素的容器中(如排序算法) | 
| std::equality_comparable<T> | T类型支持==和!=操作符 | 用于相等比较,如查找元素、集合操作 | 
| std::totally_ordered<T> | T类型支持<,<=,>,>=操作符 | 用于有序数据结构(如排序算法、排序容器) | 
| std::ranges::range<T> | T是一个范围(range)类型,可以用于基于范围的算法 | 适用于范围操作的函数和算法 | 
| std::invocable<F, Args...> | F类型可以用Args...参数调用 | 用于函数对象、回调函数和策略设计模式 | 
| std::predicate<F, Args...> | F是一个谓词函数,且F(Args...)可以转换为bool | 适用于谓词判断的算法,如 std::find_if | 
| std::is_arithmetic<T>::value | T是一个算术类型(包括整数和浮点数) | 常用于计算类模板、加减乘除运算 | 
| std::is_standard_layout<T>::value | T是标准布局类型 | 适用于需要直接操作内存布局的结构体 | 
| std::is_trivial<T>::value | T是平凡类型,可以用memcpy等操作进行复制 | 数据序列化、内存拷贝的操作 | 
| std::is_copy_assignable<T>::value | T类型支持拷贝赋值操作 | 需要拷贝赋值的场景(如集合中的元素替换) | 
| std::is_move_assignable<T>::value | T类型支持移动赋值操作 | 适用于优化复制性能的容器 | 
一个例子:C++ 库中的模版参数限制
在使用 stack 库或者 vector 库时,使用形如以下的定义会报错:
std::stack<MyClass &> s;
std::vector<MyClass &> v;2
这是因为 stack 和 vector 的模版参数要求类型 T 必须是是可拷贝构造和可拷贝赋值的,而 MyClass & 是一个引用类型。在 C++ 中,引用类型是不可拷贝赋值的,因此会导致编译错误。
如果我们自己实现相似的库的话,我们就可以使用 Concept 来限制模版参数的类型,以提前避免这种错误。
元编程 Meta Programming 
元编程是利用模版来进行编译时静态生成代码的技术。某种意义上可以理解为增强版的 inline 和宏定义。利用元编程在编译时生成代码,可以提高代码的性能和可读性。
例如,我们可以生成这样的计算阶乘的代码:
template <int N>
struct factorial {
    enum { value = N * factorial<N - 1>::value };
};
template <>
struct factorial<0> {
    enum { value = 1 };
};2
3
4
5
6
7
8
9
这当中,factorial 是一个递归的模版,它会在编译时展开,生成一个阶乘的计算过程。我们可以在编译时直接得到 factorial<5>::value 的值。
在上面的例子中,我们还使用了模版特化的技术。模版特化是指我们可以为特定的类型参数提供特定的实现,以覆盖原有的模版实现,它的标志是 template <>,之后紧接着的是我们希望特化的类型(要在尖括号中指定),然后是我们的特化实现。
此外,你可能还注意到了 :: 运算符。在 C++ 中,:: 运算符用于访问类的静态成员,也可以用于访问模版的静态成员。换句话说,:: 是全局的解析,不关联具体的对象;而相对的,. 是某个对象的成员访问。
我们还可以使用 constexpr 关键字来声明一个编译时常量,同样可以用于元编程。例如,我们可以这样定义一个编译时计算阶乘的函数:
constexpr int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}
int main() {
    constexpr int x = factorial(5);
    return 0;
}2
3
4
5
6
7
8
可以看出,使用 constexpr 关键字更加像是在定义一个普通的函数,但是它的返回值是一个编译时常量。这样的函数在调用时,其参数应当是一个常量,其返回值也只能传递给一个常量。这就是 C++ 中的字面类型。
利用函数指针 
前文中我们介绍了模版这一强大的工具。但尽管模版提供了很强的泛型能力,似乎在某些方面自定义的能力还不够强。例如,如果我们想实现一个排序算法,我们经常需要自定义比较函数。一种想法是,使用操作符重载,这种方法我们会在 
下面我们给出一个冒泡排序的函数,作为对照,我们同时给出原始版:
void MySort(void *base, unsigned width, unsigned num, int (*compare)(const void *elem1, const void *elem2)) {
    char *A = (char *) base;
    char *tmp = new char[width];
    for (unsigned i = 1; i < num; i++) {
        for (unsigned j = 0; i < num - i; i++)
            if (compare(A + j * width, A + (j + 1) * width) > 0) {
                memcpy(tmp, A + j * width, width);
                memcpy(A + j * width, A + (j + 1) * width, width);
                memcpy(A + (j + 1) * width, tmp, width);
            }
    }
    delete tmp;
}2
3
4
5
6
7
8
9
10
11
12
13
void MySort(int *A, int n) {
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < n - i; j++)
            if (A[j] > A[j + 1]) {
                int tmp = A[j];
                A[j] = A[j + 1];
                A[j + 1] = tmp;
            }
    }
}2
3
4
5
6
7
8
9
10
我们可以看到,在泛型版中,我们使用了一个函数指针 int (*compare)(const void *elem1, const void *elem2) 作为比较函数的参数;在函数体的比较部分,我们可以直接调用 compare 函数来进行比较。这样,我们可以在调用 MySort 函数时,传入一个自定义的比较函数,来实现不同的排序方式。
有些时候,我们传入的比较函数比较简单,可以考虑使用下面介绍的 
C++ 11
C++ 11 引入。其主要语法如下:
[capture_list] (parameter_list) specifiers exception -> type {function body}
其中:
- capture_list:指定- [=]:以值捕获的方式捕获所有外部变量。
- [&]:以引用捕获的方式捕获所有外部变量。
- [this]:捕获当前类的- this指针。
- [a, &b]:对变量- a值捕获,对变量- b引用捕获。
 
- parameter_list:定义- specifiers:指定- multable、- static等。
- exception:指定- noexception。
下面是几个 
#include <iostream>
int main() {
    auto add = [](int a, int b) { return a + b; };
    std::cout << "3 + 4 = " << add(3, 4) << std::endl;
    return 0;
}2
3
4
5
6
7
#include <iostream>
int main() {
    int x = 10;
    int y = 20;
    // 以值捕获x,以引用捕获y
    auto lambda = [x, &y]() {
        // x的值被捕获,因此在lambda表达式中x是不可修改的
        // y被引用捕获,因此可以修改y的值
        std::cout << "x = " << x << ", y = " << y << std::endl;
        y = 30;
    };
    lambda();
    std::cout << "y after lambda = " << y << std::endl;
    return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
:::
除了这种带有比较函数的排序算法可以用到函数指针,我们还可以在一些模版类中要求传入一个函数指针,以实现一些自定义的功能。例如,我们可以在前文的 ArrayList 模版中传入一个自定义的打印函数,这样就可以新增一个成员函数,来打印整个数组。下面是在 ArrayList 中添加一个打印函数的例子:
template<typename T>
struct ArrayList {
    T *array;
    int capacity; // allocated size
    int size; // actual element occupation 
    int elem_size; // size of each element
    void (*print)(T); // print function
    ArrayList() {
    ArrayList(void (*print)(T)) {
        size = 0;
        capacity = 10;
        elem_size = sizeof(T);
        array = static_cast<T *>(malloc(capacity * elem_size));
        this->print = print;
    }
    void add(T x) {
        ...... // the same
    }
    void remove(T x) {
        ...... // the same
    }
    std::optional<T> get(int index) {
        ...... // the same
    }
    int getSize() {
        return size;
    }
    int getCapacity() {
        return capacity;
    }
    
    void printArrayList(){
        for(int i = 0; i < size; i++){
            print(array[i]);
            cout<< " ";
        }
    }
};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
32
33
34
35
36
37
38
39
40
41
42
43
44
这样,我们就可以在创建 ArrayList 对象时,传入一个打印函数,来实现自定义的打印功能。
