可变参数列表#

可变参数宏#

__VA_ARGS__ 原样替换传入的参数列表。

宏写法示例

#define PRINTF(...) printf(__VA_ARGS__)

展开前调用

PRINTF("a + b = %d", a + b);

宏展开结果

printf("a + b = %d", a + b);

#__VA_ARGS__ 将整个参数列表转换为字符串。注意参数本身也会被引号包裹。

宏写法示例

#define PRINTF(...) printf(#__VA_ARGS__)

展开前调用

PRINTF(1, "x", int);

宏展开结果

printf("1, \"x\", int");

##__VA_ARGS__ 如果没有额外参数,则会移除前面多余的逗号;有参数则保留。

宏写法示例

#define PRINTF(fmt, ...) printf(fmt, ##__VA_ARGS__)

展开前调用

PRINTF("a = %d", a);

宏展开结果

printf("a = %d", a);

GCC 扩展语法(非标准),等同于 __VA_ARGS__,不推荐使用。

宏写法示例

#define PRINTF(x...) printf(x)

展开前调用

PRINTF("a + b = %d", a + b);

宏展开结果

printf("a + b = %d", a + b);

实战案例:https://gitee.com/zhyantao/misc/blob/master/leetcode/cpp/include/debug.h

可变参数函数展开(C风格)#

在上一节中,我们通过宏将可变参数直接传递给已有函数(如 printf)。这一节介绍如何在自定义函数中访问和处理可变参数列表。

关键概念

术语

含义

va_list

类型,用于保存可变参数列表。

va_start

宏,初始化 va_list,使其指向第一个可变参数。

va_arg

宏,从参数列表中提取下一个参数(需指定类型)。

va_end

宏,清理 va_list 占用的资源。

vsnprintf

函数,用于将格式化字符串写入缓冲区,支持 va_list 参数。

示例 1:日志函数 log

#include <cstdarg>
#include <iostream>

void log(char* fmt, ...) {
    char buf[512] = {0};
    va_list ap;

    va_start(ap, fmt);                              // 初始化参数列表
    (void)vsnprintf(buf, sizeof(buf) - 2, fmt, ap); // 使用 vsnprintf 格式化输出
    va_end(ap);                                     // 清理参数列表

    printf("%s\n", buf);
}

int main() {
    log((char*)"%s, %d, %s", "hello", 100, "world");
}

示例 2:求和函数 sum

#include <cstdarg>
#include <iostream>

double sum(int num, ...) {
    va_list ap; // 定义参数列表
    double ret = 0.0;

    va_start(ap, num); // 初始化参数列表

    for (int i = 0; i < num; i++) {
        ret += va_arg(ap, double); // 按类型依次取出参数
    }

    va_end(ap); // 清理参数列表

    return ret;
}

int main() {
    std::cout << "Sum of 2, 3 is " << sum(2, 2.0, 3.0) << std::endl;
    std::cout << "Sum of 2, 3, 4, 5 is " << sum(4, 2.0, 3.0, 4.0, 5.0) << std::endl;
}

getopt_long#

#include <getopt.h>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
    /**
     * struct option
     * {
     *      const char * name;
     *      int          has_arg;
     *      int        * flag;
     *      int          val;
     * }
     */
    static struct option long_options[] = {
        {"reqarg", required_argument, NULL, 'r'},
        {"optarg", optional_argument, NULL, 'o'},
        {"noarg", no_argument, NULL, 'n'},
        {NULL, 0, NULL, 0},
    };

    while (1) {
        int option_index = 0, opt;

        /**
         * getopt_long 会遍历 argv 数组,在每次迭代中,它会返回下一个选项字符(如果有),
         * 然后,这个字符与长选项列表进行比较,如果匹配,则对应的操作会被执行,
         * 如果没有匹配,那么函数将返回 -1。
         * 只有一个字符,不带冒号,只表示选项,如 -c
         * 一个字符,后接一个冒号,表示选项后面带一个参数,如 -a 100
         * 一个字符,后接两个冒号——表示选项后面带一个可选参数,
         * 即参数可有可无,如果带参数,则选项与参数直接不能有空格,如 -b200
         */
        opt = getopt_long(argc, argv, "a::b:c:d", long_options, &option_index);

        if (opt == -1) // 选项遍历完毕,退出循环
            break;

        printf("opt = %c\t\t", opt);
        printf("optarg = %s\t\t", optarg);
        printf("optind = %d\t\t", optind);
        printf("argv[%d] = %s\t\t", optind, argv[optind]);
        printf("option_index = %d\n", option_index);
    }

    return 0;
}

这段代码是一个使用 getopt_long 函数进行命令行参数解析的例子,它演示了如何处理短选项(使用单个字符)和长选项(使用字符串)。

首先,代码定义了一个静态的 struct option 数组 long_options,用于描述长选项的信息。每个数组元素都是一个结构体,包含以下字段:

  • name:选项的名称,字符串类型。

  • has_arg:指定选项是否需要参数,有三个可能值:no_argument(0)表示不需要参数,required_argument(1)表示必须有参数,optional_argument(2)表示参数是可选的。

  • flag:如果不为 NULL,则指向一个整数变量,用于存储选项的值(即 val 字段),而不是返回选项字符。如果为 NULL,则 getopt_long 函数将返回选项字符。

  • val:选项的值,通常是一个字符。

main 函数中,使用一个 while 循环来遍历命令行参数。在循环中,调用 getopt_long 函数来获取下一个选项。getopt_long 的参数包括:

  • argcargv:分别是命令行参数的数量和数组。

  • "a::b:c:d":短选项的字符串表示,冒号表示需要参数的选项。

  • long_options:长选项的数组。

  • option_index:用于存储当前长选项在 long_options 数组中的索引。

在每次循环迭代中,打印出当前选项的相关信息,包括选项字符、选项参数、optind 的值、对应的参数值等。

g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out --reqarg 100 --optarg=200 --noarg
g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out –reqarg=100 --optarg=200 --noarg
g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out --reqarg 100 --optarg --noarg

这条指令表示使用 C++20 标准,进行优化,启用所有警告,使用多线程,编译 main.cpp 文件,然后运行生成的可执行文件 a.out 并传递一些命令行参数。

在不同的命令行调用中,通过使用 --reqarg--optarg--noarg 等选项来测试程序的输出。程序会解析这些选项,并打印相关的信息。

<typename... Args>#

在 C++ 中,你可以使用递归或者使用 C++17 引入的折叠表达式(fold expression)来访问可变参数列表中的每个参数。

#include <iostream>

// 递归终止条件
void printArgs() {
    std::cout << std::endl;
}

// 递归步骤
template <typename T, typename... Args>
void printArgs(T first, Args... args) {
    std::cout << first << " ";
    printArgs(args...); // 递归调用
}

int main() {
    printArgs(1, "Hello", 3.14, 'A');
    return 0;
}

在这个例子中,printArgs 函数通过递归的方式遍历可变参数列表,打印每个参数的值。递归终止条件是一个没有参数的版本。

#include <iostream>

template <typename... Args>
void printArgs(Args... args) {
    (std::cout << ... << args) << std::endl; // 折叠表达式
}

int main() {
    printArgs(1, "Hello", 3.14, 'A');
    return 0;
}

在这个例子中,使用了 C++17 引入的折叠表达式。(std::cout << ... << args) 表示将所有参数展开成一个表达式,然后通过 << 运算符连接起来,最后加上 std::endl 进行换行。

这两种方法都允许你访问可变参数列表中的每个参数,具体选择取决于你的编译环境和代码的需求。折叠表达式提供了一种更简洁和直观的语法,但需要 C++17 及以上的编译器支持。

包展开(args…)和模式(args)

例 1:字面量作为实参

template <typename... Us>
void f(Us... pargs) {}

template <typename... Ts>
void g(Ts... args) {
    // &args... 是包展开
    // &args    是它的模式
    f(&args...);
}

int main() {
    // Ts... args   会展开成 int E1, double E2, const char* E3
    // &args...     会展开成 &E1, &E2, &E3
    // Us...        会展开成 int* E1, double* E2, const char** E3
    g(1, 0.2, "a");
}

例 2:数组作为实参

// 接受任意数量的模板参数(用 Ts... 表示)
template <typename... Ts>
void f(Ts...) {}

// 接受一个模板参数包 Ts 和一个非类型参数包 N
// Ts (&...arr)[N] 表示 arr 是一个引用数组,数组元素的类型是 Ts
// 数组的大小是 N
template <typename... Ts, int... N>
void g(Ts (&... arr)[N]) {}

int main() {
    // Ts... 会展开成 void f(char, int)
    f('a', 1);

    // Ts... 会展开成 void f(double)
    f(0.1);

    // Ts (&...arr)[N] 会展开成 const char (&)[2], int (&)[1]
    // 模板参数 Ts 被展开为 const char
    // 非类型参数 N 被展开为数组大小,即 2 和 1
    int n[1];
    g<const char, int>("a", n);
}

例 3:调整可变参数的位置

template <typename A, typename B, typename... C>
void func(A arg1, B arg2, C... arg3) {
    container<A, B, C...> t1; // 展开成 container<A, B, E1, E2, E3>
    container<C..., A, B> t2; // 展开成 container<E1, E2, E3, A, B>
    container<A, C..., B> t3; // 展开成 container<A, E1, E2, E3, B>
}