C++ 可变参数

前言

在C语言中,printf是一个我们常用的函数,我们可以无限传入不同类型的参数,这里就用到了可变参数。我们一般使用形参包...或者是std::initializer_list来实现可变参数。不仅函数可以有可变参数,模板也可以有。因此形参包一般可以分为模板形参包和函数形参包。

形参包

函数形参包

声明

形参包的声明就是英文省略号,即三个点(...)。比如如下声明就是没问题的,在C和C++中都可以通过编译:

void print(const char* fmt, ...);

如果是C++的话,可以省略前面那个逗号。下面的声明只能在C++中编译通过,C语言会报错。所以为了与C的兼容性,还是建议使用第一个声明。

void print(const char* fmt...);

要注意的是,形参包只能放在形参列表的末尾。下面的声明就是错误的

void print(..., const char* fmt); //incorrect

形参列表只包含形参包是合法的,但是我们后续就无法访问实参,原因下文会讲到。

void print(...); //correct, but the params could not be used

访问实参

在 <stdarg.h> 中提供了va_listva_startva_argva_copyva_end这一个类型和四个宏函数来实现实参的访问:

  • va_list:一个类型,保有 va_start、va_arg、va_end 和 va_copy所需的信息
  • va_start:启用对可变函数实参的访问
  • va_arg:访问下一个可变函数实参
  • va_copy:制造可变函数实参的副本
  • va_end:结束对可变函数实参的遍历

在访问之前声明一个va_list类型的变量,比如args

void func(int count,...){
    va_list args;
}

之后调用va_start初始化,第一个参数传入args,第二个传入形参包前一个参数,这里是count,然后在最后调用va_end停止遍历,这就是刚才我们说的如果只在参数列表中声明...不能访问的原因:

void func(int count,...){
    va_list args;
    va_start(args, count);
    // do something here
    va_end(args);
}

va_copy可以拷贝一个va_list到另一个va_list:

    std::va_list args1;
    va_start(args1, count);
    std::va_list args2;
    va_copy(args2, args1);

va_arg 需要两个参数,第一个是va_list类型,比如刚才的args,然后是需要获取的类型。每调用一次,都会修改args使其指向下一个参数。如果下个参数没有访问则是默认值,比如int类型就是0。下面有个加法函数的例子:

#include <iostream>
#include <cstdarg>

int add(int count, ...){
    int sum = 0;
    std::va_list args;
    va_start(args, count);
    for (int i = 0; i < count; i++) {
        int num = va_arg(args, int);
        sum += num;
    }
    va_end(args);
    return sum;

}

int main(){
    std::cout << add(5, 62 , 13, 7, 45, 11) << '\n';
}

输出内容为:

138

可变参数相关C风格IO函数

我们以std::vsprintf为例,剩下的还有std::vprintfstd::vfprintfstd::vsnprintf等等,具体可以参考C-style file input/output - cppreference.com。vsprintf是一个把变参格式化并打印到字符串的函数,这样我们可以自己写一个print函数:

#include <iostream>
#include <cstdarg>

void print(const char* fmt, ...){
    char buf[1024];
    std::va_list args;
    va_start(args, fmt);
    vsprintf(buf, fmt, args);
    va_end(args);

    printf("%s",buf);
}

int main(){
    print("My name is %s. And I'm %d years old.","Chris",17);
}

输出内容如下:

My name is Chris. And I'm 17 years old.

在宏函数中应用可变参数

我们依旧使用形参包...传参,但是我们要使用__VA_ARGS__访问,并且需要传入另一个函数。这时我们就可以直接只在参数列表中声明...,比如:

#define log(...) print(__VA_ARGS__)
void print(const char* fmt, ...){
    char buf[1024];
    std::va_list args;
    va_start(args, fmt);
    vsprintf(buf, fmt, args);
    va_end(args);

    printf("%s",buf);
}

模板形参包

声明

声明如下:

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

包展开

template<class... Us>
void f(Us... pargs) {}
 
template<class... Ts>
void g(Ts... args)
{
    f(&args...); // “&args...” 是包展开
                 // “&args” 是它的模式
}
 
g(1, 0.2, "a"); // Ts... args 会展开成 int E1, double E2, const char* E3
                // &args... 会展开成 &E1, &E2, &E3
                // Us... 会展开成 int* E1, double* E2, const char** E3

如果两个形参包在同一模式中出现,那么它们同时展开而且长度必须相同:

template<typename...>
struct Tuple {};
 
template<typename T1, typename T2>
struct Pair {};
 
template<class... Args1>
struct zip
{
    template<class... Args2>
    struct with
    {
        typedef Tuple<Pair<Args1, Args2>...> type;
        // Pair<Args1, Args2>... 是包展开
        // Pair<Args1, Args2> 是模式
    };
};
 
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>... 会展开成
// Pair<short, unsigned short>, Pair<int, unsigned int> 
// T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>
 
typedef zip<short>::with<unsigned short, unsigned>::type T2;
// 错误:包展开中的形参包包含不同长度

如果包展开内嵌于另一个包展开中,那么它所展开的是在最内层包展开出现的形参包,并且在外围(而非最内层)的包展开中必须提及其它形参包:

template<class... Args>
void g(Args... args)
{
    f(const_cast<const Args*>(&args)...); 
    // const_cast<const Args*>(&args) 是模式,它同时展开两个包(Args 与 args)
 
    f(h(args...) + args...); // 嵌套包展开:
    // 内层包展开是 “args...”,它首先展开
    // 外层包展开是 h(E1, E2, E3) + args 它其次被展开
    // (成为 h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}

获取参数数量

#include <iostream>
using namespace std;

template<typename... Args>
int getArgNums(Args... args){
    return sizeof...(args);
}

int main(){
    cout<<getArgNums("Hello World",666,'c');
    return 0;
}

结果输出如下:

3

使用递归的方式遍历

可变参数可以使用递归的方式进行遍历,每次从可变参数中取出第一个元素,然后向下一个参数推导,直到包为空。比如:

#include <iostream>
using namespace std;

template<typename T>
void print(T arg){
    cout << arg << endl;
}

template<typename T, typename... Args>
void print(const T &arg, Args... args){
    cout << arg << endl;
    print(args...);
}

int main(){
    print("Hello World",666,'c');
    return 0;
}

结果输出如下:

Hello World
666
c

std::initializer_list

C++另外提供了initializer_list来实现变参。不过传参必须使用花括号括起来,而且只能是单一类型,然后可以使用迭代器访问:

#include <iostream>
#include <string>
using namespace std;

void print(initializer_list<string> ls) {
    for (auto i = ls.begin(); i != ls.end(); ++i)
        cout << *i << " ";
    cout << endl;
}

int main(){
    print({"Hello World","lalala","cpp"});
    return 0;
}

输出如下:

Hello World lalala cpp

参考:cppreference.com

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

orbitgw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值