欢迎光临
我们一直在努力

C++可变模板参数详细讲解

先给你一句终极人话(背下来,这就是全部)

Args = 你随便起的一个名字,跟 int a 里的 a 一样,想改成 ABC、Params、Shit 都行,没有任何魔法。

… = 只有两个功能:打包、拆包

  • 写在左边:…Args → 意思是打包:“这是一堆类型,我先装成一包”
  • 写在右边:args… → 意思是拆包:“把刚才那一包,一个一个拆出来用”
  • 我只盯着这段代码讲,别的一概不提

    线程池里这段,是你唯一要懂的:

    template<typename F, typename… Args>
    void submit(F&& f, Args&&… args) {
    auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)…);
    }

    我逐字、逐符号,翻译成人话。


    第一行:template<typename F, typename… Args>

    1. typename F

    就是:我要接收一个 “要执行的函数 / 任务”,给它起个名叫 F。

    2. typename… Args

    • … 在左边 → 打包
    • Args 是包名, 代表着大量的各种各样的参数类型名
    • 整句翻译:我还要接收 “一堆随便多少个、随便什么类型的参数”,我把这一堆打包,包名叫 Args

    “一堆” 可以是:

    • 0 个
    • 1 个:int
    • 2 个:int + string
    • 3 个:int + string + double
    • 100 个都行

    这里的 … 就是:允许传 “一堆”,而不是固定一个。

    你完全可以写成:

    template<typename F, typename… ABC>
    void submit(F&& f, ABC&&… abc)

    功能一模一样,Args 就是个名字,别把它当神仙。


    第二行:void submit(F&& f, Args&&… args)

    1. F&& f

    对应上面的 F:那个要执行的函数本体,叫 f。

    2. Args&&… args

    • Args:上面打包的那 “一堆类型”
    • … 在左边 → 继续打包
    • args:给 “这一堆参数的本体” 起的名字
    • 整句翻译:这是跟上面那包类型对应的、一堆真实的参数,叫 args

    大白话:

    • Args = 一包类型(int、string、double…)
    • args = 一包数值(10、"hello"、3.14…)

    第三行:std::forward<Args>(args)…

    这里的 … 在最右边 → 拆包!

    翻译:

    把刚才打包好的 args 这一包参数,一个一个拆出来,挨个传给 std::bind。

    我给你看「编译器实际做了什么」,你瞬间就懂

    你写一行通用代码,编译器会根据你传的参数,自动生成对应代码。

    例子 1:你提交 1 个参数

    pool.submit(函数, 10);

    编译器看到后,自动把你的模板展开成:

    template<typename F, typename… Args>
    // Args 被自动推导为:int
    void submit(F&& f, int&& args) {
    auto task = std::bind(…, std::forward<int>(args));
    }

    例子 2:你提交 2 个参数

    pool.submit(Calculate(), 5, "square");

    编译器自动展开成:

    template<typename F, typename… Args>
    // Args 被自动推导为:int, const char*
    void submit(F&& f, int&& args1, const char*&& args2) {
    auto task = std::bind(
    …,
    std::forward<int>(args1),
    std::forward<const char*>(args2)
    );
    }

    例子 3:你提交 3 个参数

    pool.submit(func, 1, 2.2, "abc");

    编译器自动展开成:

    template<typename F, typename… Args>
    // Args = int, double, const char*
    void submit(F&& f, int&& a1, double&& a2, const char*&& a3) {
    auto task = std::bind(
    …,
    std::forward<int>(a1),
    std::forward<double>(a2),
    std::forward<const char*>(a3)
    );
    }

    现在告诉你:为什么要写这玩意?

    如果不写 …,你必须给 1 个参数、2 个参数、3 个参数… 各写一个函数:

    // 不支持可变参,你要写死人
    void submit(F f) { … }
    void submit(F f, A1 a1) { … }
    void submit(F f, A1 a1, A2 a2) { … }
    void submit(F f, A1 a1, A2 a2, A3 a3) { … }

    写了 typename… Args + Args&&… args + args…你写 1 行,编译器自动帮你写 1000 行重载。

    这就是它唯一的作用。


    我再把三个 … 汇总,用最蠢的话再说一遍

    只看这三句,记住就毕业:

  • typename… Args→ 我要收一堆类型,打包,包名叫 Args

  • Args&&… args→ 我要收一堆对应的值,打包,包名叫 args

  • args…→ 把 args 这一包,拆成一个一个,挨个用

  • C++ 把 **“打包” 和 “拆包” 用同一个符号 … 表示 **,写在左边是打包,写在右边是拆包,长得一模一样,所以看起来很复杂;

    这不是你的问题,是 C++ 语法设计得反人类。


    最后给你一个 “傻瓜版写法”,你以后照抄就行

    你以后写任意 “接收随便多少参数” 的函数,固定照抄这个模板:

    template<typename F, typename… 随便起个名>
    void 函数名(F&& f, 随便起个名&&… 随便起个值名) {
    用到参数的地方写:随便起个值名…
    }

    放到线程池,就是:

    template<typename F, typename… Args>
    void submit(F&& f, Args&&… args) {
    auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)…);
    }

    C++11递归式调用

    示例 1:

    基础版 —— 打印任意数量、任意类型的参数(无返回值)

    这是最直观的例子,能清晰看到参数包的定义和展开逻辑:

    #include <iostream>
    #include <string>

    // 步骤1:定义「递归终止函数」(必须!参数包拆空时调用)
    // 作用:当参数包没有参数时,结束递归
    void print_all() {
    std::cout << "\\n参数打印完成\\n";
    }

    // 步骤2:定义可变参数模板函数(核心)
    // T:第一个参数的类型;Args… rest:剩余参数的参数包
    template<typename T, typename… Args>
    void print_all(T first, Args… rest) {
    // 1. 处理第一个参数(类型安全,编译器知道T的类型)
    std::cout << "参数:" << first << " | 类型:" << typeid(T).name() << " ";

    // 2. 递归调用:把剩余参数包传进去,逐步拆解
    print_all(rest…); // "rest…" 是参数包展开,拆成单个参数
    }

    // 测试:传任意数量、任意类型的参数
    int main() {
    // 测试1:传int、string、double、bool
    print_all(100, std::string("hello"), 3.14, true);

    // 测试2:传0个参数(触发递归终止函数)
    print_all();

    // 测试3:传自定义类型(可变参数模板的优势——支持任意类型)
    struct Person { std::string name; int age; };
    // 给Person重载<<,才能打印
    std::ostream& operator<<(std::ostream& os, const Person& p) {
    return os << "Person(" << p.name << "," << p.age << ")";
    }
    print_all(Person{"张三", 20}, Person{"李四", 25});

    return 0;
    }

    输出示例(类型名因编译器不同略有差异):

    参数:100 | 类型:int 参数:hello | 类型:basic_string<char,…> 参数:3.14 | 类型:double 参数:1 | 类型:bool
    参数打印完成

    参数打印完成

    参数:Person(张三,20) | 类型:Person 参数:Person(李四,25) | 类型:Person
    参数打印完成

    核心语法解释:
    • template<typename T, typename… Args>:定义模板参数包,Args 表示 “任意数量的类型”;
    • void print_all(T first, Args… rest):函数参数包,first 是第一个参数,rest 是剩余参数的集合;
    • print_all(rest…):参数包展开,把 rest 拆成单个参数传给下一次递归;
    • 递归终止函数 print_all():参数包拆空时必须有这个函数,否则编译器会报错(递归无法终止)。

    示例 2:

    进阶版 —— 求和任意数量的数值型参数(有返回值)

    展示如何处理 “有返回值 + 类型限制” 的场景(比如只允许数值类型):

    #include <iostream>
    #include <type_traits> // 用于类型检查

    // 递归终止:0个参数时返回0
    template<typename T = int>
    constexpr T sum_all() {
    return 0;
    }

    // 可变参数模板:求和任意数值类型参数
    template<typename T, typename… Args>
    auto sum_all(T first, Args… rest) -> std::enable_if_t<std::is_arithmetic_v<T>, decltype(first)> {
    // 静态断言:确保所有参数都是数值类型(int/double/float等)
    static_assert((std::is_arithmetic_v<Args> && …), "所有参数必须是数值类型!");

    // 递归求和:第一个参数 + 剩余参数的和
    return first + sum_all(rest…);
    }

    int main() {
    // 测试1:int + double
    std::cout << "1+2.5+3 = " << sum_all(1, 2.5, 3) << std::endl; // 6.5

    // 测试2:仅1个参数
    std::cout << "100 = " << sum_all(100) << std::endl; // 100

    // 测试3:错误示例(传字符串,触发静态断言)
    // std::cout << sum_all(1, "hello") << std::endl; // 编译报错:所有参数必须是数值类型!

    return 0;
    }

    C++17 折叠表达式(无递归,推荐)

    折叠表达式是 C++17 专门为参数包设计的语法,能直接 “展开” 参数包并执行批量操作(比如打印、求和),完全不需要递归。

    示例 3:无递归打印任意参数(最直观)

    #include <iostream>
    #include <string>

    // 无递归!折叠表达式直接展开参数包
    template<typename… Args>
    void print_all(Args&&… args) {
    // 折叠表达式核心:(操作, …) 或 (… , 操作)
    // 这里是:对每个参数执行 cout << args << " ",用逗号分隔
    (std::cout << … << args) << " | 打印完成\\n";
    }

    int main() {
    // 测试任意参数
    print_all(100, std::string("hello"), 3.14, true);
    print_all(); // 0个参数也能处理(折叠表达式空展开,无输出)
    return 0;
    }

    输出:

    100hello3.141 | 打印完成
    | 打印完成

    折叠表达式核心语法解释:

    (std::cout << … << args) 是二元左折叠,等价于:

    // 如果传 100, "hello", 3.14 → 展开为:
    std::cout << 100 << "hello" << 3.14;

    折叠表达式的通用格式:

    • 左折叠:(init op … op args) → 从左到右展开(比如 (a + … + args));
    • 右折叠:(args op … op init) → 从右到左展开;
    • 空折叠:参数包为空时,数值类型返回 0,逻辑类型返回 true,流操作无输出(安全)。

    示例 2:无递归求和任意数值参数

    #include <iostream>
    #include <type_traits>

    // 无递归求和:折叠表达式 + 类型检查
    template<typename… Args>
    auto sum_all(Args&&… args) {
    // 静态断言:确保所有参数都是数值类型
    static_assert((std::is_arithmetic_v<Args> && …), "必须传数值类型!");
    // 折叠表达式求和:1 + 2 + 3 → (1 + (2 + 3))
    return (… + args);
    }

    int main() {
    std::cout << "1+2.5+3 = " << sum_all(1, 2.5, 3) << std::endl; // 6.5
    std::cout << "仅1个参数:" << sum_all(100) << std::endl; // 100
    std::cout << "0个参数:" << sum_all() << std::endl; // 0(空折叠默认值)
    return 0;
    }

    示例 3:无递归处理复杂逻辑(比如每个参数执行自定义操作)

    如果需要对每个参数执行多步操作(比如打印值 + 类型),可以用「折叠表达式 + lambda」:

    #include <iostream>
    #include <string>
    #include <typeinfo>

    template<typename… Args>
    void print_with_type(Args&&… args) {
    // 对每个参数执行lambda,用逗号折叠(执行多个操作)
    (
    [&](auto&& arg) {
    std::cout << "值:" << arg << " | 类型:" << typeid(arg).name() << "\\n";
    }(std::forward<Args>(args)),

    );
    }

    int main() {
    print_with_type(100, std::string("hello"), 3.14);
    return 0;
    }

    输出(类型名因编译器略有差异):

    值:100 | 类型:int
    值:hello | 类型:basic_string<char,…>
    值:3.14 | 类型:double

    递归 vs 折叠表达式 对比

    方式C++ 版本代码简洁度可读性效率适用场景
    递归 C++11+ 中等 中等 兼容旧编译器、复杂逻辑
    折叠表达式 C++17+ 极高 极高 现代编译器、简单 / 复杂逻辑
    赞(0)
    未经允许不得转载:171主机测评 » C++可变模板参数详细讲解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址