C/C++中的可变参数和可变参数模板


目录
  • 1、说明
  • 2、C语言中的可变参数
  • 3、C++中的可变参数模板
    • 2.1、使用递归的方式遍历
    • 2.2、使用非递归的方式遍历

1、说明

不谈官方定义,就从个人理解上说,可变参数 就是函数传参的时候,不确定传入参数的数量和类型,从而动态地在函数内部处理,优点是,函数调用时比较灵活

2、C语言中的可变参数

C语言中一般使用宏定义实现可变参数,先看一个示例:

#include 
void func(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    auto a = va_arg(ap, int);
    auto b = va_arg(ap, double);
    auto c = va_arg(ap, char*);
    
    cout << a << ", " << b << ", " << c << endl;
    va_end(ap);
}
int main()
{
    func("%d %f %s\n", 1, 2.0f, "hello world");
    return 0;
}

这是一个很常见的C语言的可变参数的使用,va_start 用于初始化 va_list 变量,va_arg 用于提取可变参数,va_end 用于释放 va_list

这个示例可以用在一般函数上,无法使用在宏定义中,如果一定要在宏定义中使用,需要配合 __VA_ARGS__,示例如下:

//#define CALC(fmt, ...) func(fmt, ...) //错误的使用
#define CALC(fmt, ...) func(fmt, __VA_ARGS__)
int main()
{
    CALC("%d %f %s\n", 1, 2.0f, "hello world");
    return 0;
}

3、C++中的可变参数模板

C++11 中引入了新的功能,可变参数模版,语法如下:

template 
void func(T t,Args ... args);

这里面,Args 称之为模板参数包(template parameter pack),表示模板参数位置上的变长参数,

args 称之为函数参数包(function parameter pack),表示函数参数位置上的变长参数

可以使用 sizeof...() 获取可变参数数目

先看一个示例:

template
void print(Args... args)
{
    int num = sizeof...(args);
}
int main()
{
    print(1, 2, "123", 4);
    return 0;
}

执行结果是:

4

2.1、使用递归的方式遍历

可变参数一般使用递归的方式进行遍历,利用模板的推导机制,每次从可变参数中取出第一个元素,直到包为空

缺点:递归毕竟是使用栈内存,过多的递归层级容易导致爆栈的发生

示例代码如下:

void printf()
{
    cout << "end" << endl;
}
template
void print(const T &value, Args... args)
{
    cout << value << endl;
    print(args...);
}
int main()
{
    print(1, 2, "333", 4);
    return 0;
}

结果输出如下:

1
2
333
4
end

这里不是很好理解,我们可以自己理解 print() 方法的每一次调用

  1. main函数中第一次调用,value为1, args有2、"333和4三个值,输出1;
  2. 第一次递归,即print中调用print,value为2,args有“333”和4两个值,输出2;
  3. 第二次递归,即print中调用print,value为“333”,args为4,输出“333”;
  4. 第三次递归,即print中调用print,value为4,args无值,输出4;
  5. 此时,args因为无值,print(args...) 语句调用的就不再是模板函数,而是第一行的 print(),输出end;

所以,很好理解,为什么要先定义一个同名的函数,就是为了等可变参数经过几次推导之后,没有值的情况出现;

当然,递归遍历也可以这么写:

template
void print(T value)
{
    cout << "end:" << value << endl;
}
template
void print(const T &value, Args... args)
{
    cout << value << endl;
    print(args...);
}
int main()
{
    print(1, 2, "333", 4);
    return 0;
}

结果输出:

1
2
333
end:4

这和第一个例子只有些许区别,函数调用如下:

  1. main函数中第一次调用,value为1, args有2、"333和4三个值,输出1;
  2. 第一次递归,即print中调用print,value为2,args有“333”和4两个值,输出2;
  3. 第二次递归,即print中调用print,value为“333”,args为4,输出“333”;
  4. 此时,args为4,print(args...) 语句调用的就不再是模板函数,而是第一行的 print(4),输出end:4;

2.2、使用非递归的方式遍历

利用 std::initializer_list ,即初始化列表展开可变参数

示例1,使用展开函数处理参数:

template
void run(const T &t)
{
    cout << t << endl;
}
template
void print(Args... args)
{
    std::initializer_list{(run(args), 0)...};
}
int main()
{
    print(1, 2, "333as", 4);
    return 0;
}

示例2,使用lambda:

template
void print(Args... args)
{
    std::initializer_list{([&]
    { cout << args << endl; }(), 0)...};
}
int main()
{
    print(1, 2, "333as", 4);
    return 0;
}