夜雨聆风学习资料网

ARTICLE · 1213935

C++模版编程演进

C++模版编程演进

C++98/03:混沌初开

模板设计的初衷:实现 泛型容器 (比如std::vector<T> )。之后意外发现:利用模板特化和递归,居然可以在编译期做计算!

核心机制

  • 类模板与函数模板 :基础的泛型能力。
  • 模板特化 / 偏特化 :相当于编译期的 if-else 。
  • SFINAE(Substitution Failure Is Not An Error) :“替换失败不是错误”。早期做编译期类型检查的唯一手段,极其晦涩。

解决了什么问题

  • 代码复用(如 STL 容器)。
  • 初步的编译期计算(如编译期求阶乘)。

痛点(恶梦)

  • 没有可变参数,想传任意数量的参数得写几十个重载(如早期的 boost::bind )。
  • SFINAE 写法极其反人类(各种 enable_if 、 sizeof 魔法)。
  • 报错信息长达几百行,根本看不懂。

C++11:现代模板的奠基(革命性升级)

一次彻底的革命,引入了大量特性,让模板编程真正具备了工业可用性。

引入的关键特性

可变参数模板(Variadic Templates)

作用:可以接收任意数量、任意类型的模板参数( template<typename… Args> )。可以优雅地写出 std::tuple 和完美的 printf 替代品了,不再需要手写 1 到 10 个参数的重载。

尾置返回类型(Trailing Return Type)

作用 :让编译器帮忙推导表达式的类型。


1
2


// 在泛型编程中,如果不知道 t1 + t2 会返回什么类型,用 decltype 能完美解决。
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)

别名模板(Alias Templates / using )

作用 :替代晦涩的 typedef 。比如 :


1
2


template<typename T> 
using MyVec = std::vector<T, MyAlloc<T>>; 

这是在 typedef 时代是做不到的( typedef 不支持模板)。

std::enable_if 标准化

作用 :把 SFINAE 这种黑魔法正式纳入标准库,提供了一种相对标准的条件编译方式。

外部模板(Extern Templates)

作用 :减少模板实例化带来的编译时间爆炸。


1
2


// 在头文件里声明:告诉编译器“别在这里实例化,别处有现成的”
extern template class std::vector<HeavyType>;

用一次集中的、显式的实例化定义,换取所有其他编译单元里隐式实例化工作的豁免。不改变程序的运行时行为,纯粹是编译期和链接期的空间与时间优化。

C++14:更好用的小修小补

解决很多 C++11 遗留的“别扭”问题。

引入的关键特性

变量模板(Variable Templates)

解决的问题 :允许模板定义变量,而不仅是类和函数。比如


1
2
3
4


template <typename T> constexpr DrawableVTable vtable_for = {...}

// 定义编译期常量
template<class T> constexpr T pi = T(3.1415926535897932385); 

普通函数的返回类型推导(Return Type Deduction)

解决的问题 :不再强制要求写尾置 decltype 。直接写


1


auto add(T1 t1, T2 t2){ return t1 + t2; }

泛型 Lambda(Generic Lambdas)


1


[] (auto x, auto y) { return x + y; } 

C++17:从“元编程”走向“编译期编程”

核心目标是: 干掉晦涩的模板递归和 SFINAE,让编译期代码看起来像普通的运行时代码。

引入的关键特性

constexpr if (编译期 if) : 终结了大量 SFINAE 和模板特化! 以前为了在编译期做条件分支,必须写多个模板重载;现在直接在一个函数里写:


1
2


// 编译器会自动把不需要的分支丢弃掉
if constexpr (std::is_integral_v<T>){ ... } else { ... }

折叠表达式(Fold Expressions) : 终结了处理可变参数模板时的递归! 以前展开 Args… 必须写递归函数和终止条件;现在直接写 (args + …) 就能把所有参数加起来。


1
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
45
46
47
48
49
50
51
52
53
54
55
56
57


#include <type_traits>

// 旧版 C++11/14
template<typename... Args>
auto sum(Args... args){
    using T = std::common_type_t<Args..., int>;   // 加 int 保证空包也有类型
    T result{};
    int dummy[] = { 0, (result += args, 0)... };  // 加 0 保证数组非空
    (void)dummy;
    return result;
}

// 新版 C++17s
template<typename... Args>
auto sum(Args... args){
    return (0 + ... + args);   // 等价
}

/* 打印参数 */
// 旧版 C++11/14
void print(){ std::cout << '\n'; }   // 终止条件

template<typename T, typename... Args>
void print(T first, Args... rest){
    std::cout << first << ' ';
    print(rest...);
}

// 新版 C++17
template<typename... Args>
void print(Args... args){
    ((std::cout << args << ' '), ...);   // 逗号折叠
    std::cout << '\n';
}


/* 检查是否所有参数都满足某个条件 */
// 旧版 C++11/14
bool all_true(){ return true; }

template<typename... Args>
bool all_true(bool first, Args... rest){
    return first && all_true(rest...);
}

// 新版 C++17
template<typename... Args>
bool all_true(Args... args){
    return (args && ...);   // 所有为 true 才返回 true
}

/* 在容器里依次插入元素 */
// 新版 C++17
template<typename Container, typename... Args>
void push_all(Container& c, Args... args){
    (c.push_back(args), ...);   // 依次 push_back
}

特别注意:折叠表达式有一个二元左右折叠的注意细节,这里不展开介绍。

类模板参数推导(CTAD) :实例化模板类时不需要再手写尖括号了。以前必须写 std::pair<int, double> p(1, 2.0); ,现在直接写 std::pair p(1, 2.0); ,编译器自动推导。这也是为什么 std::make_pair 等辅助函数逐渐退出历史舞台的原因。

内联变量( inline variables) :不是专门针对模板,解决了 header-only 模板库中全局变量重复定义的痛点。

C++20:概念(Concepts),模板的最终形态

引入了 C++ 历史上酝酿最久(超过 15 年)的特性:Concepts。它彻底改变了模板编程的思维方式。

引入的关键特性:

 Concepts 与 Requires 表达式: 彻底终结 SFINAE! 以前模板就像是一个没有类型检查的黑盒,只有在实例化报错时你才知道传错了类型,而且报错信息是极其恐怖的“瀑布式报错”。


1
2
3
4
5
6
7
8
9


// 有了 Concepts,你可以像定义接口一样约束模板参数。

//定义约束:
template<typename T> 
concept Addable = requires(T a, T b) { a + b; };

//使用约束
template<Addable T>      
T add(T a, T b){ return a + b; }

模板支持 Lambda :现在可以把 Lambda 表达式直接塞进模板参数里(比如作为 std::set 或智能指针的自定义删除器),无需再单独定义一个仿函数类。


1
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71



#include <iostream>
#include <set>
#include <memory>
#include <vector>
#include <algorithm>

int main(){
    // ============================================================
    // 场景 1:Lambda 作为 std::set 的比较器
    // ============================================================
    // 以前:必须单独定义一个 struct,重载 operator()
    //  struct Greater { bool operator()(int a, int b) const { return a > b; } };
    //   std::set<int, Greater> s;
    //
    // 现在:直接传 lambda 的类型作为模板参数
    auto cmp = [](int a, int b) { return a > b; };   // 降序比较
    std::set<int, decltype(cmp)> s(cmp);
    s.insert({3, 1, 4, 1, 5, 9, 2, 6});
    std::cout << "set (descending): ";
    for (int x : s) std::cout << x << ' ';
    std::cout << '\n';

    // ============================================================
    // 场景 2:Lambda 作为 unique_ptr 的自定义删除器
    // ============================================================
    // 管理 malloc 分配的内存,用 free 释放
    auto free_deleter = [](int* p) {
        std::cout << "freeing " << *p << '\n';
        std::free(p);
    };

    // decltype(free_deleter) 作为 unique_ptr 的第二个模板参数
    // Lambda 的类型是匿名的,必须用 `decltype` 取
    std::unique_ptr<int, decltype(free_deleter)> p1(
        static_cast<int*>(std::malloc(sizeof(int))),
        free_deleter
    );
    *p1 = 42;

    // ============================================================
    // 场景 3:Lambda 作为 shared_ptr 的删除器(更简洁)
    // ============================================================
    // shared_ptr 的删除器类型不需要出现在模板参数里,直接用 lambda

    std::shared_ptr<FILE> fp(
        std::fopen("/tmp/test_lambda.txt", "w"),
        [](FILE* f) {
            if (f) {
                std::cout << "closing file\n";
                std::fclose(f);
            }
        }
    );
    if (fp) {
        std::fputs("hello from lambda deleter\n", fp.get());
    }

    // ============================================================
    // 场景 4:Lambda 作为 sort 的自定义比较(非模板参数,但常见)
    // ============================================================
    std::vector<int> v = {5, 2, 8, 1, 9};
    std::sort(v.begin(), v.end(), [](int a, int b) {
        return a % 2 < b % 2;   // 偶数在前,奇数在后
    });
    std::cout << "sorted by parity: ";
    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';

    return 0;
}

更强大的 constexpr :现在你甚至可以在编译期动态分配内存、使用 STL 容器做复杂的计算,计算完后把结果固化在只读数据段里。


1
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


#include <vector>
#include <array>
#include <iostream>

// C++20:constexpr 函数里可以用 std::vector
constexpr auto build_primes(int n){
    std::vector<int> primes;
    for (int i = 2; i <= n; ++i) {
        bool is_prime = true;
        for (int p : primes) {
            if (p * p > i) break;
            if (i % p == 0) { is_prime = false; break; }
        }
        if (is_prime) primes.push_back(i);
    }
    return primes;   // 返回 vector,编译期计算
}

int main(){
    // 编译期计算,结果固化
    constexpr auto primes = build_primes(50);
    static_assert(primes.size() == 15);   // 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47
    static_assert(primes[0] == 2);
    static_assert(primes[14] == 47);

    for (int p : primes) std::cout << p << ' ';
}

更贴近实际使用经验的例子


1
2
3
4
5
6
7
8
9
10
11
12
13
14


// C++ 17 
constexpr auto build_primes_17(){
    std::array<int, 15> primes{}; // 预先知道结果有多少个元素。 15个
    int count = 0;
    // ... 手动管理 count,不能 push_back
    return primes;
}

// C++ 20
constexpr auto build_primes_20(int n){
    std::vector<int> primes; //不需要预知
    // ... primes.push_back(i)
    return primes;
}

一些限制:

  • 编译期分配的内存不能泄露
  • 不是所有 std::vector 操作都能在编译期用
  • 编译期求值有步数限制
  • 编译时间会变长

总结:演进的主线

回顾这段历史,C++ 模板编程的演进主线非常清晰:

C++98 / C++11 :解决了“能不能做”的问题。引入可变参数、 decltype 。

C++11 / C++17 :解决了“好不好写”的问题。引入 if constexpr 、折叠表达式,干掉特化。

C++17 / C++20 :解决了“好不好读、安不安全”的问题。引入 Concepts,规范模板契约,消灭报错瀑布。

相关学习资料