第三章:模板参数进阶——非类型模板参数、模板模板参数、可变参数模板的声明与展开

模板参数,说白了就是模板的「输入」。大多数人都用过类型模板参数,比如 typename T。但C++模板的能力远不止于此。我个人习惯把模板参数分成三类:类型参数、非类型参数、模板参数。今天我们就来聊聊后两种,再加上一个让模板变得极其灵活的可变参数模板。

3.1 非类型模板参数:不只是类型

模板参数不一定非得是类型。它可以是一个整数、一个枚举、一个指针,甚至是一个 std::nullptr_t。这就是非类型模板参数。

举个例子,你想写一个固定大小的数组包装器:

template<typename T, std::size_t Size>
class FixedArray {
    T data[Size];
public:
    constexpr std::size_t size() const { return Size; }
    T& operator[](std::size_t i) { return data[i]; }
};

// 使用
FixedArray<int, 10> arr;  // 编译期就知道大小

我在项目中遇到过一个问题:用 std::array 时,大小必须是编译期常量。非类型模板参数正好解决了这个痛点。你想想看,如果大小是运行时传入的,那编译器就没法做优化了。

核心要点:非类型模板参数必须是编译期常量表达式。支持的类型包括:整型、枚举、指针、引用、std::nullptr_t,以及C++20开始支持的浮点型和字面量类类型。

避坑指南:我曾经在项目里用 double 作为非类型模板参数,结果发现C++20之前是不支持的。如果你还在用C++17,老老实实用整型吧。

3.2 模板模板参数:模板的模板

模板模板参数,顾名思义,就是模板参数本身也是一个模板。听起来有点绕,但用起来很直观。

假设你想写一个容器适配器,它接受一个容器类型作为参数:

template<typename T, template<typename> class Container>
class Adapter {
    Container<T> container;
public:
    void push(const T& value) { container.push_back(value); }
    // ...
};

// 使用
Adapter<int, std::vector> vecAdapter;  // 注意:std::vector有两个模板参数,这里需要适配

嗯,这里要注意。标准库容器通常有多个模板参数(比如分配器)。直接传 std::vector 会编译失败,因为 std::vector 实际上需要两个参数。解决办法是使用别名模板:

template<typename T>
using MyVector = std::vector<T, std::allocator<T>>;

Adapter<int, MyVector> adapter;

我个人习惯在写泛型库时,尽量让模板模板参数灵活一些。比如允许用户指定默认分配器,而不是硬编码。

3.3 可变参数模板:数量不定的参数

可变参数模板是C++11引入的,它允许模板接受任意数量的参数。说白了,就是参数包(parameter pack)。

template<typename... Args>
void print(Args... args) {
    // args 是一个参数包
}

这里 Args 是类型参数包,args 是函数参数包。你可以传任意数量的参数:

print(1, 2.0, "hello", 'c');  // 四个参数,类型各不相同

3.4 参数包的展开:核心难点

参数包本身不能直接使用,必须展开。展开的方式有很多种,我挑几个最常用的讲。

3.4.1 递归展开

这是最经典的方式,通过递归函数模板来逐个处理参数:

// 递归终止条件
void print() {}

// 递归展开
template<typename T, typename... Args>
void print(const T& first, const Args&... rest) {
    std::cout << first << " ";
    print(rest...);  // 展开 rest 包
}

// 调用
print(1, 2.0, "hello");  // 输出:1 2 hello

我在项目中遇到过一个问题:递归展开在参数很多时会导致模板实例化爆炸,编译时间会变长。后来我改用折叠表达式(C++17),效率高多了。

3.4.2 折叠表达式(C++17)

折叠表达式让参数包展开变得极其简洁。它分为左折叠和右折叠:

// 一元右折叠
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // 展开为 ((a + b) + c) + ...
}

// 一元左折叠
template<typename... Args>
auto sumLeft(Args... args) {
    return (... + args);  // 展开为 a + (b + (c + ...))
}

// 二元折叠(带初始值)
template<typename... Args>
auto sumWithInit(Args... args) {
    return (0 + ... + args);  // 初始值0
}

小技巧:折叠表达式支持几乎所有二元运算符。比如用 && 做逻辑与,用 , 做逗号表达式。我个人最常用的是逗号表达式来执行多个操作:

template<typename... Args>
void printAll(Args... args) {
    ((std::cout << args << " "), ...);  // 逗号折叠
}

3.4.3 模式展开

有时候你需要对每个参数应用一个模式。比如把每个参数都包装成 std::make_tuple:

template<typename... Args>
auto makeTupleOfTuples(Args... args) {
    return std::make_tuple(std::make_tuple(args)...);
    // 展开为 std::make_tuple(std::make_tuple(a), std::make_tuple(b), ...)
}

你想想看,这种模式展开在元编程中非常有用。比如你想对每个类型应用一个元函数:

template<typename... Types>
struct TypeList {
    // 对每个类型应用 std::add_pointer
    using PointerList = std::tuple<typename std::add_pointer<Types>::type...>;
};

3.5 综合示例:一个简单的元组实现

把上面的知识串起来,我们实现一个简化版的 std::tuple:

// 基础模板
template<typename... Types>
class Tuple;

// 递归终止:空元组
template<>
class Tuple<> {};

// 递归定义
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
    Head value;
public:
    Tuple() = default;
    Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}

    Head& getHead() { return value; }
    const Head& getHead() const { return value; }

    Tuple<Tail...>& getTail() { return *this; }
    const Tuple<Tail...>& getTail() const { return *this; }
};

// 获取第N个元素的辅助函数
template<std::size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
    if constexpr (Index == 0) {
        return t.getHead();
    } else {
        return get<Index - 1>(t.getTail());
    }
}

这个例子用到了非类型模板参数(std::size_t Index)、可变参数模板(typename... Types)、以及递归展开。我在实际项目中就用类似的方式实现过一个轻量级的类型安全容器。

3.6 知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系:

模板参数进阶知识体系 模板参数 非类型模板参数 模板模板参数 可变参数模板 整型、枚举、指针、引用 C++20: 浮点、字面量类 容器适配器、策略模式 别名模板简化使用 参数包声明与展开 递归展开、折叠表达式 模式展开、元组实现 三者结合:构建灵活、高效的泛型库

3.7 实战建议

学完这些知识,你可能会问:什么时候用?我给出几个实际场景:

  • 非类型模板参数:编译期常量,比如数组大小、位宽、循环次数。我在写嵌入式代码时经常用它来配置硬件寄存器地址。
  • 模板模板参数:当你需要抽象出容器或策略时。比如写一个通用的缓存系统,允许用户指定底层存储结构。
  • 可变参数模板:实现类型安全的变参函数、元组、访问者模式。说白了,只要参数数量不确定,就用它。

性能提醒:可变参数模板的递归展开会导致大量模板实例化。如果参数数量很大(比如超过100个),编译时间和二进制体积会显著增加。我曾经在一个日志库中用了递归展开,结果编译时间从10秒飙到了3分钟。后来改用折叠表达式,问题解决了。

好了,这一章的内容就到这里。记住,模板参数进阶是元编程的基石。掌握了非类型参数、模板模板参数和可变参数模板,你就能写出更通用、更灵活的代码。

可可精品资料库