【c++面向对象编程】第43篇:可变参数模板(C++11):优雅处理不定长参数

目录

一、为什么需要可变参数模板?

二、基本语法

[参数包(Parameter Pack)](#参数包(Parameter Pack))

[sizeof... 运算符](#sizeof... 运算符)

三、参数包展开

方式1:递归展开(最直观)

方式2:初始化列表展开(C++11,更高效)

[四、折叠表达式(C++17 简化)](#四、折叠表达式(C++17 简化))

五、完美转发与可变参数模板

[六、tuple 的实现原理](#六、tuple 的实现原理)

[简化版 tuple 实现](#简化版 tuple 实现)

[七、完整例子:类型安全的 printf](#七、完整例子:类型安全的 printf)

八、常见错误

[1. 忘记递归终止函数](#1. 忘记递归终止函数)

[2. 在运行时使用 sizeof...](#2. 在运行时使用 sizeof...)

[3. 参数包展开顺序错误](#3. 参数包展开顺序错误)

九、这一篇的收获


一、为什么需要可变参数模板?

在 C++98/03 中,实现一个任意数量参数的函数很麻烦:

cpp

复制代码
// 方式1:重载(最多 N 个)
void print() {}
void print(int a) {}
void print(int a, int b) {}
void print(int a, int b, int c) {}
// 永远不够...

// 方式2:使用 va_list(类型不安全)
#include <cstdarg>
void badPrint(const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);
    // 容易出错,没有类型检查
    vprintf(fmt, args);
    va_end(args);
}
badPrint("%d %s %f", 42, "hello", 3.14);  // 格式字符串必须匹配

可变参数模板:类型安全、编译期检查、无限参数。

cpp

复制代码
template<typename... Args>
void printAll(Args... args) {
    // 可以处理任意数量的参数,每个都有自己的类型
}

printAll(1, "hello", 3.14, 'c');  // 编译期展开

二、基本语法

参数包(Parameter Pack)

cpp

复制代码
// Args 是类型参数包
template<typename... Args>
class Tuple {};

// args 是函数参数包
template<typename... Args>
void func(Args... args) {}

// 使用
Tuple<int, double, string> t;  // Args = {int, double, string}
func(1, 2.5, "hello");         // args = {1, 2.5, "hello"}

sizeof... 运算符

获取参数包中的参数个数(编译期常量):

cpp

复制代码
template<typename... Args>
void countArgs(Args... args) {
    cout << "类型数量: " << sizeof...(Args) << endl;
    cout << "参数数量: " << sizeof...(args) << endl;
}

countArgs(1, "hello", 3.14);  // 类型数量: 3,参数数量: 3
countArgs();                   // 类型数量: 0,参数数量: 0

三、参数包展开

参数包不能直接使用,需要通过展开获取每个元素。主要有两种展开方式。

方式1:递归展开(最直观)

cpp

复制代码
// 终止函数:空参数时调用
void print() {
    cout << endl;
}

// 递归函数:处理第一个参数,然后递归处理剩余
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    cout << first << " ";   // 处理第一个
    print(rest...);          // 递归处理剩余
}

int main() {
    print(1, 2.5, "hello", 'c');  // 输出: 1 2.5 hello c
}

展开过程

text

复制代码
print(1, 2.5, "hello", 'c')
  → cout << 1; print(2.5, "hello", 'c')
    → cout << 2.5; print("hello", 'c')
      → cout << "hello"; print('c')
        → cout << 'c'; print()
          → 换行

方式2:初始化列表展开(C++11,更高效)

利用数组初始化列表的求值顺序保证(从左到右),一次性展开所有参数:

cpp

复制代码
template<typename... Args>
void printAll(Args... args) {
    // 使用 int 数组的初始化列表来触发展开
    int dummy[] = { (cout << args << " ", 0)... };
    // 或者更优雅的方式:
    // (cout << ... << args);  // C++17 折叠表达式,后面会讲
}

printAll(1, 2.5, "hello");  // 输出: 1 2.5 hello

解释

  • (cout << args << " ", 0) 是一个逗号表达式:输出 args,然后返回 0

  • { (expr)... }expr 对每个 args 展开,生成 {0, 0, 0} 数组

  • 数组不需要使用,只是为了触发展开


四、折叠表达式(C++17 简化)

C++17 引入了折叠表达式,让参数包展开更加简洁:

cpp

复制代码
// 一元右折叠
template<typename... Args>
void printAll(Args... args) {
    (cout << ... << args) << endl;  // ((cout << arg1) << arg2) << ...
}

// 带分隔符
template<typename... Args>
void printWithComma(Args... args) {
    ((cout << args << (sizeof...(args) == 1 ? "" : ", ")), ...);
    cout << endl;
}

// 求和
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // 右折叠:arg1 + (arg2 + (arg3 + ...))
}

int main() {
    printAll(1, 2.5, "hello");     // 1 2.5 hello
    cout << sum(1, 2, 3, 4) << endl;  // 10
}
折叠类型 语法 展开结果
一元右折叠 (args + ...) (arg1 + (arg2 + (arg3 + ...)))
一元左折叠 (... + args) (((arg1 + arg2) + arg3) + ...)
二元折叠 (args + ... + init) 带初始值

五、完美转发与可变参数模板

在库开发中,常常需要将参数包完美转发给另一个函数:

cpp

复制代码
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
    // 完美转发每个参数
    return unique_ptr<T>(new T(forward<Args>(args)...));
}

// 使用
auto p = make_unique<string>(10, 'a');  // 等价于 new string(10, 'a')

展开过程
forward<Args>(args)... 展开为 forward<Arg1>(arg1), forward<Arg2>(arg2), ...


六、tuple 的实现原理

std::tuple 是可变参数模板的经典应用,可以存储任意多个不同类型的值。

简化版 tuple 实现

cpp

复制代码
#include <iostream>
#include <typeinfo>
using namespace std;

// 前向声明
template<typename... Types>
class Tuple;

// 空 tuple 的特化(终止条件)
template<>
class Tuple<> {};

// 递归定义:一个头部 + 尾部
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
private:
    Head value;
    
public:
    Tuple() : value(), Tuple<Tail...>() {}
    
    Tuple(const Head& h, const Tail&... t) 
        : value(h), Tuple<Tail...>(t...) {}
    
    // 获取头部
    Head& head() { return value; }
    const Head& head() const { return value; }
    
    // 获取尾部
    Tuple<Tail...>& tail() { return *this; }
    const Tuple<Tail...>& tail() const { return *this; }
};

// 获取 tuple 中第 N 个元素(编译期递归)
template<size_t N, typename Tuple>
struct TupleElement;

template<typename Head, typename... Tail>
struct TupleElement<0, Tuple<Head, Tail...>> {
    using type = Head;
    static Head& get(Tuple<Head, Tail...>& t) {
        return t.head();
    }
};

template<size_t N, typename Head, typename... Tail>
struct TupleElement<N, Tuple<Head, Tail...>> {
    using type = typename TupleElement<N-1, Tuple<Tail...>>::type;
    static type& get(Tuple<Head, Tail...>& t) {
        return TupleElement<N-1, Tuple<Tail...>>::get(t.tail());
    }
};

// get 函数模板
template<size_t N, typename... Types>
auto& get(Tuple<Types...>& t) {
    return TupleElement<N, Tuple<Types...>>::get(t);
}

// 打印 tuple(递归展开)
template<size_t N, typename... Types>
void printTuple(const Tuple<Types...>& t) {
    if constexpr (N < sizeof...(Types)) {
        cout << get<N>(t);
        if constexpr (N + 1 < sizeof...(Types)) {
            cout << ", ";
        }
        printTuple<N+1>(t);
    }
}

template<typename... Types>
void print(const Tuple<Types...>& t) {
    cout << "(";
    printTuple<0>(t);
    cout << ")" << endl;
}

int main() {
    Tuple<int, double, string> t(42, 3.14, "hello");
    
    cout << get<0>(t) << endl;  // 42
    cout << get<1>(t) << endl;  // 3.14
    cout << get<2>(t) << endl;  // hello
    
    print(t);  // (42, 3.14, hello)
    
    return 0;
}

原理

  • Tuple<A, B, C> 继承自 Tuple<B, C>,再继承自 Tuple<C>,再继承自 Tuple<>

  • 每个层级存储一个元素(value

  • get<N> 通过模板递归找到第 N 层的元素


七、完整例子:类型安全的 printf

cpp

复制代码
#include <iostream>
#include <string>
using namespace std;

// 基础情况:没有参数时只输出格式字符串中的普通字符
void formatPrint(const char* fmt) {
    while (*fmt) {
        if (*fmt == '%' && *(fmt + 1) == '%') {
            ++fmt;
        }
        cout << *fmt;
        ++fmt;
    }
}

// 递归处理:匹配 % 并输出对应参数
template<typename T, typename... Args>
void formatPrint(const char* fmt, T value, Args... args) {
    while (*fmt) {
        if (*fmt == '%') {
            ++fmt;
            if (*fmt == '%') {
                cout << '%';
                ++fmt;
                continue;
            }
            // 输出当前参数
            cout << value;
            // 递归处理剩余参数
            formatPrint(fmt + 1, args...);
            return;
        }
        cout << *fmt;
        ++fmt;
    }
}

// 辅助宏(或函数)
template<typename... Args>
void myPrintf(const char* fmt, Args... args) {
    formatPrint(fmt, args...);
}

int main() {
    myPrintf("Hello, %!\n", "world");
    myPrintf("% + % = %\n", 1, 2, 3);
    myPrintf("Percent sign: %%\n");
    myPrintf("Mixed: % is %.2f, and %\n", 42, 3.14159, "done");
    
    return 0;
}

输出:

text

复制代码
Hello, world!
1 + 2 = 3
Percent sign: %
Mixed: 42 is 3.14, and done

八、常见错误

1. 忘记递归终止函数

cpp

复制代码
// ❌ 缺少空参数版本,递归不会终止
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    cout << first;
    print(rest...);  // 当 rest 为空时找不到匹配
}

2. 在运行时使用 sizeof...

cpp

复制代码
int n = sizeof...(args);  // ✅ 编译期常量
// 但不能这样:
if (sizeof...(args) > 0) { ... }  // ✅ 可以,编译期判断

3. 参数包展开顺序错误

cpp

复制代码
// 初始化列表展开的顺序是确定的(从左到右)
int arr[] = { (cout << args, 0)... };  // 顺序正确

// 但函数参数的求值顺序不确定
func(func1(args...), func2(args...));  // 危险

九、这一篇的收获

你现在应该理解:

  • 可变参数模板template<typename... Args>,接受任意数量和类型的参数

  • sizeof...:获取参数包大小(编译期常量)

  • 递归展开:用终止函数 + 递归模板处理参数包

  • 初始化列表展开 :利用 { (expr, 0)... } 一次性展开

  • 折叠表达式 (C++17):(args + ...) 简化展开

  • tuple 原理 :递归继承 + 特化实现,get<N> 编译期索引

💡 小作业:实现一个 makeArray 函数,接受任意多个参数,返回 std::array(需要 C++17)。要求:auto arr = makeArray(1, 2, 3, 4); 得到 array<int, 4>{1,2,3,4}。提示:需要推导数组大小。


下一篇预告 :第44篇《typename与class的区别,依赖类型名与template消除歧义》------模板中 typenameclass 大多数时候可以互换,但有种情况必须用 typename:告诉编译器"这是个类型"。template 关键字也有类似的消歧义作用。下篇讲清楚这些细节。

相关推荐
Hanniel1 小时前
Python __slots__ 入门指南
开发语言·python·性能优化
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第69题】【JVM篇】第29题:GC Roots 有哪些?
java·开发语言·jvm·面试
William Dawson1 小时前
【通俗易懂!Spring四大核心注解源码解读:@Configuration、@ComponentScan、@Import、@EnableXXX实战】
java·后端·spring
Matlab程序猿小助手1 小时前
【MATLAB源码-第319期】基于matlab的帝王蝶优化算法(MBO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab
Tigshop开源商城1 小时前
Tigshop 开源商城系统 JAVA v5.8.28 版本发布|『角色权限管理+店铺后台跳转逻辑』优化
java·开源商城系统·tigshop
码点滴1 小时前
CRI-O选型与容器运行时标准
开发语言·人工智能·架构·kubernetes·cri-o
回眸&啤酒鸭1 小时前
【回眸】嵌入式软件单元测试工具链实战指南
开发语言·单元测试·白盒测试
彦为君1 小时前
JavaSE-10-并发编程(11个案例)
java·开发语言·python·ai·nio
石山代码1 小时前
java前景
java·开发语言