需要预先学习:


1. 可变长参数模版

普通模版的参数数量是固定的——template <typename T1, typename T2> 只能接受两个类型。而可变长参数模版(variadic template)打破了这一限制:可以写出接受任意数量、任意类型参数的模版。

这是 C++11 引入的特性,std::tuple、std::make_shared、std::thread 的构造函数等大量标准库组件都是基于它实现的。


1.1 基础语法

可变长参数模版的核心是参数包(parameter pack)。用 typename... Args 声明一个类型参数包,用 Args... args 声明一个函数参数包:

template <typename... Args>
void print(Args... args)
{
    // args 是一个参数包,包含调用时传入的全部实参
}

Args 是一个类型参数包(type parameter pack),代表零个或多个类型;args 是一个函数参数包(function parameter pack),代表零个或多个函数实参。调用 print(1, 2.0, "hello") 时,Args 展开为 int, double, const char*,args 展开为 1, 2.0, "hello"。


1.2 包展开

包展开(pack expansion)的语法是 模式...——在参数包名后加 ...,编译器会把"模式"应用到参数包中的每个元素:

template <typename... Args>
void forward_all(Args... args)
{
    // args...   —— 将参数包原样展开
    foo(args...);       // 展开为 foo(a1, a2, a3)

    // &args...  —— 对每个参数取地址
    foo(&args...);      // 展开为 foo(&a1, &a2, &a3)

    // f(args)... —— 对每个参数调用 f()
    foo(args)           // 展开为 foo(a1); foo(a2); foo(a3)
}

最常见的用法是在函数调用中展开参数包:

template <typename... Args>
void call_with_header(Args... args)
{
    std::cout << "调用开始\n";
    some_function(args...);  // 将所有参数原封不动地转发
}

1.3 sizeof... 运算符

想知道参数包中有多少个参数,使用 sizeof... 运算符:

template <typename... Args>
void count(Args... args)
{
    std::cout << "参数个数: " << sizeof...(Args) << '\n';
    // 等价于 sizeof...(args)
}

int main()
{
    count();              // 输出 0
    count(1, 2, 3);       // 输出 3
    count("a", 3.14, 1);  // 输出 3
}

1.4 递归展开

处理参数包的经典模式是递归展开——每次取出"第一个"参数,递归处理剩余参数,直到参数包为空时走终止条件。

#include <iostream>

// 终止条件:参数包为空时调用
void print_all()
{
    std::cout << '\n';
}

// 递归:处理第一个参数,递归处理剩余
template <typename T, typename... Args>
void print_all(T first, Args... args)
{
    std::cout << first << ' ';
    print_all(args...);  // 递归调用,参数包少了一个
}

int main()
{
    print_all(42, 3.14, "hello", 'X');
}

展开过程:

调用 第一个参数 剩余参数
print_all(42, 3.14, "hello", 'X') 42 3.14, "hello", 'X'
print_all(3.14, "hello", 'X') 3.14 "hello", 'X'
print_all("hello", 'X') "hello" 'X'
print_all('X') 'X' —
print_all() — —(终止)

2. 折叠表达式(C++17)

递归展开虽然通用,但写法稍显啰嗦。C++17 引入了折叠表达式(fold expression),可以在一条语句中对参数包执行逐元运算。


2.1 四种折叠形式

语法 展开结果 名称
(... op args) (a1 op a2) op a3 一元左折叠
(args op ...) a1 op (a2 op a3) 一元右折叠
(init op ... op args) ((init op a1) op a2) op a3 二元左折叠
(args op ... op init) a1 op (a2 op (a3 op init)) 二元右折叠

2.2 求和:从递归到折叠

用递归方式对参数包求和,需要写多个重载;用折叠表达式则一行搞定:

// 递归方式(C++11)
template <typename T>
T sum(T v)
{
    return v;
}

template <typename T, typename... Args>
T sum(T first, Args... args)
{
    return first + sum(args...);
}

// 折叠表达式
template <typename... Args>
auto sum_fold(Args... args)
{
    return (... + args);  // 一元左折叠
}

int main()
{
    std::cout << sum_fold(1, 2, 3, 4, 5) << '\n';  // 输出 15
}

其它运算符同样适用—— (... && args) 判断全部为真, (... , args) 依次执行每个表达式:

template <typename... Args>
bool all_true(Args... args)
{
    return (... && args);
}

template <typename F, typename... Args>
void for_each(F f, Args... args)
{
    (f(args), ...);  // 逗号折叠:对每个参数调用 f
}

int main()
{
    std::cout << std::boolalpha;
    std::cout << all_true(true, true, false) << '\n';  // 输出 false

    for_each([](auto x) { std::cout << x << ' '; }, 1, 2, 3);
    // 输出:1 2 3
}

3. 变参类模版

类模版同样可以使用可变长参数。最典型的例子是 std::tuple 的简化实现。


3.1 递归继承实现 Tuple

// 主模版:至少有一个元素
template <typename T, typename... Rest>
class Tuple : private Tuple<Rest...>
{
public:
    Tuple(T v, Rest... rest)
        : Tuple<Rest...>(rest...), m_value(v) {}

    T head() const { return m_value; }

private:
    T m_value;
};

// 特化:空参数包,作为递归终点
template <>
class Tuple<>
{
    // 空——递归终止
};

int main()
{
    Tuple<int, double, std::string> t(42, 3.14, "hello");
}

3.2 变参类模版的偏特化

处理变参类模版时,偏特化经常用来从参数包中"提取模式":

// 判断一个类型是否在类型列表中
template <typename Search, typename... Types>
struct Contains;

// 偏特化:参数包不为空,检查第一个
template <typename Search, typename Head, typename... Tail>
struct Contains<Search, Head, Tail...>
{
    static constexpr bool value =
        std::is_same_v<Search, Head> || Contains<Search, Tail...>::value;
};

// 偏特化:参数包为空,没找到
template <typename Search>
struct Contains<Search>
{
    static constexpr bool value = false;
};

int main()
{
    std::cout << Contains<int, double, int, char>::value << '\n';     // 输出 1 (true)
    std::cout << Contains<float, double, int, char>::value << '\n';   // 输出 0 (false)
}

4. 实际应用


4.1 完美转发变参

结合 std::forward 和可变长参数模版,可以实现完美转发——将参数原样传递给另一个函数。

详见 -完美转发

例如一个 make_unique 的自定义实现,使用 std::forward<Args>(args)... 同时对每个参数进行完美转发:

#include <memory>

template <typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

4.2 emplace 系列函数

std::vector::emplace_back 也是用可变长参数模版实现的——它接受任意数量和类型的参数,直接在容器内部构造对象,避免拷贝:

#include <vector>
#include <string>

int main()
{
    std::vector<std::string> words;
    words.emplace_back("hello");               // 构造 "hello"
    words.emplace_back(5, 'x');                // 构造 "xxxxx"
    words.emplace_back(words[0].begin(),
                       words[0].end());        // 从迭代器区间构造

    for (auto& w : words)
        std::cout << w << '\n';
    // 输出:
    // hello
    // xxxxx
    // hello
}