乐于分享
好东西不私藏

C++ 可变参数模版

C++ 可变参数模版

什么是"可变参数模板"

可变参数模板(variadic template)可以接受任意个数的参数:
template<typename... Args>void func(Args... args) { }   // 0个、1个、3个、100个都行func();              // 0个参数func(1);             // 1个:Args = {int}func(12.5"hi");  // 3个:Args = {int, double, const char*}

三个核心语法

1. typename... Args —— 声明参数包

... 是关键,意思是"这里可以有零个或多个类型":

template<typename... Args>   // Args 是"类型参数包"voidfunc(Args... args){    // args 是"值参数包"}

2. sizeof...(Args) —— 获取参数个数

template<typename... Args>void func(Args... args) {    std::cout << sizeof...(Args) << "\n";   // 编译期执行,类型数量    std::cout << sizeof...(args) << "\n";   // 编译期,值数量(和上面一样)}func();           // 0func(1);          // 1func(12.53);  // 3

注意sizeof... 是编译期常量,不是运行时函数。

3. args... —— 展开参数包

参数包不能直接用,必须"展开"——把里面每个元素取出来:

template<typename... Args>void func(Args... args) {    // args 不能直接 cout    // std::cout << args;   编译错误    // 展开方式1:递归(C++11,后面详细讲)    // 展开方式2:fold expression(C++17,后面详细讲)}

C++11:递归展开(核心难点)

C++11 展开 参数包 的唯一方法:写一个递归函数,每次剥一个参数

经典例子:打印所有参数

// 1. 递归终止条件:0个参数时调用这个voidprint(){    std::cout << "\n";}// 2. 递归本体:剥第一个参数,剩下的继续递归template<typename T, typename... Rest>void print(T first, Rest... rest) {    std::cout << first << " ";    print(rest...);   // 递归调用,参数少了一个}// 使用print(12.5"hello");// 第1次:first=1, rest={2.5, "hello"}  → 打印 "1 "// 第2次:first=2.5, rest={"hello"}      → 打印 "2.5 "// 第3次:first="hello", rest={}          → 打印 "hello "// 第4次:print() 终止                   → 打印换行

C++14:没有新语法,但泛型 lambda 让代码更简洁

C++14 本身没有给可变参数模板加新特性,但泛型 lambda 可以和参数包配合,他真正价值在于初始化捕获 可以在 lambda 里移动参数包:
template<typename... Args>voidsubmit_task(Args... args) {    auto task = [args...]() {   // C++14:拷贝捕获参数包(不能move)        process(args...);    };    task();}

C++17:Fold Expression —— 一行展开(革命性简化)

C++17 之前,展开 参数包 必须写递归(终止函数 + 递归函数 = 至少2个函数)。

C++17 的 fold expression 让你一行搞定。语法:

(args + ...)     // 一元右折叠:(arg1 + (arg2 + (arg3 + arg4)))(... + args)     // 一元左折叠:(((arg1 + arg2) + arg3) + arg4)(args + ... + 0// 二元右折叠:(arg1 + (arg2 + (arg3 + (arg4 + 0))))(0 + ... + args// 二元左折叠:((((0 + arg1) + arg2) + arg3) + arg4)

实际例子:

// 求和 —— 以前要写2个函数,现在一行template<typename... Args>intsum(Args... args) {    return (args + ...);    // 一行!}sum(123);  // 6sum(12345);  // 15// 求乘积template<typename... Args>intproduct(Args... args) {    return (args * ...);}product(234);  // 24

带初始值的二元折叠(处理空参数包)

// 空参数包问题:(args || ...)   // 空 → 编译成功 返回false(args && ...)  // 空 → 编译成功 返回true(args * ...)     // 空 → 编译错误!(args + ...)     // 空 → 编译错误!// 二元折叠:给一个初始值(args + ... + 0)   // 空参数包 → 返回 0(args * ... + 0)   // 空 → 返回 0