首页 > 编程语言 >C++开发基础——可变参数与可变参数模板

C++开发基础——可变参数与可变参数模板

时间:2024-03-21 23:59:53浏览次数:26  
标签:va ... void list C++ 参数 可变

一,可变参数

1.基础概念

可变参数在C语言和C++语言编程中都有应用。

可变参数的含义是:在函数传参的时候,参数的数量、类型都是可变的,不确定的。

在C语言中,应用到可变参数的是可变参数函数可变参数的宏

在C++语言中,C++11标准提供了两种使用可变参数的方式:

1.如果可变参数的参数类型相同,可以使用标准库中的initializer_list

2.如果可变参数的参数类型不同,可以使用可变参数模板

C语言中,在定义可变参数函数时,使用省略号"..."表示参数是可变的。

简单代码样例如下:

void printf(const char* format, …);

可变参数的使用可以让代码结构更精简。

2.可变参数相关的宏定义

在C语言中,一般需要借助相关的宏定义来实现可变参数,常见的宏定义如下:

va_arg:每一次调用va_arg会获取当前的参数,并自动更新指向下一个可变参数。

va_start:获得可变参数列表的第一个参数,开始使用可变参数列表。

va_end:结束对可变函数列表的遍历,释放va_list。

va_list:存储可变参数列表的具体信息。

    简单介绍就是,va_start用于开始使用可变参数,va_arg用于获得下一个可变参数,va_end用于释放va_list。

    它们都包含在头文件"<stdarg.h>"中

这些宏定义在具体应用时的语法如下:

type va_arg(
   va_list arg_ptr,
   type
);

void va_end(
   va_list arg_ptr
);

void va_start(
   va_list arg_ptr,
   prev_param
);

void va_start(
   arg_ptr
); // (deprecated Pre-ANSI C89 standardization version)

注意,如果自定义参数和可变参数同时在函数中出现,为了不导致编译出错,将可变参数放在形参列表的最后一个位置。

void func(char parm_1, int parm_2, ...);

完整代码样例:

#include <stdio.h>  
#include <stdarg.h>    
void vout(int max, ...)
{
    va_list arg_ptr;
    int args = 0;
    char* days[7];
    va_start(arg_ptr, max);
    while (args < max)
    {
        days[args] = va_arg(arg_ptr, char*);
        printf("Day:  %s  \n", days[args++]);
    }
    va_end(arg_ptr);
}
int main(void)
{
    vout(3, "Sat", "Sun", "Mon");
    printf("\n");
    vout(5, "Mon", "Tues", "Wed", "Thurs", "Fri");
}

运行结果:

Day:  Sat
Day:  Sun
Day:  Mon

Day:  Mon
Day:  Tues
Day:  Wed
Day:  Thurs
Day:  Fri

3.预定义标识符_VA_ARGS__

    对于可变参数相关的代码编写,除了使用省略号来表示可变参数列表,也可以使用__VA_ARGS__ 预定义标识符来表示可变参列表。

    该语法在C99标准中被引入,可以简单了解一下。

    可以用"__VA_ARGS__"表示"..."位置的所有参数,用法如下:

#define PRINT(...) printf(__VA_ARGS__)

完整代码样例:

#include <stdio.h>
#define DEBUG
#ifdef DEBUG
#define PRINT(...)  fprintf(stderr, __VA_ARGS__)
#else
#define PRINT(...)  printf(__VA_ARGS__)
#endif
int main(void)
{
    const char* s = "abc";
    int n = 123;
    PRINT("%d\n", n);
    PRINT("%s %d\n", s, n);
}

 运行结果:

123
abc 123

二,标准库模板initializer_list

initializer_list模板在函数声明中可以代表可变参数列表。

initializer_list中的参数可以使用迭代器来访问。

initializer_list实例中传入参数时需要使用{}把多个参数括起来。

代码样例:

initializer_list<int> i1{ 1, 2, 3, 4 };

Demo1:  初始化类成员

#include <iostream>
#include <vector>
#include <initializer_list>
class Point {
    std::vector<int> arr;
public:
    //Constructor accepts a initializer_list as argument
    Point(const std::initializer_list<int>& list) : arr(list)
    {}
    void display() {
        for (int i : arr)
            std::cout << i << " , ";
        std::cout << std::endl;
    }
};
int main() {
    Point pointobj({ 1, 2, 3, 4, 5 });
    pointobj.display();
    return 0;
}

运行结果:

1 , 2 , 3 , 4 , 5 ,

Demo2:结合lambda表达式一起使用

#include <iostream>
#include <initializer_list>
using namespace std;
template<typename... Args>
void print(Args... args)
{
    std::initializer_list<int>{
        ([&] { cout << args << " "; }(), 0)...
    };
}
int main()
{
    print(1, 2, "3A", 4);
    return 0;
}

运行结果:

1 2 3A 4

三,可变参数模板

1.基础概念

可变参数模板是支持任意数量和类型的参数的类模板或函数模板。

在可变参数模板中,可变数目和类型的参数列表被称为参数包(parameter pack)。

可变参数模板的参数包,分为模板参数包(template parameter pack)和函数参数包(function parameter pack)。

在模板参数位置的可变参数被称为模板参数包,在函数参数位置的可变参数被称为函数参数包。

可以使用sizeof...运算符获取参数包中具体的参数数量。

样例如下: 

//Args是一个模板参数包;args是一个函数参数包
template <typename... Args>
void func(Args... args);

如上所示,在一个模板参数列表中:

class...或typename...表示接下来的参数是零个或多个类型列表。

类型名...表示接下来的参数是零个或多个给定类型的函数参数列表。

比较一下"typename T"和"typename.. Args":

Args和T的差别是,T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。

完整代码样例:

Demo1:

#include <iostream>
template <typename T>
void printAllImpl(T item) {
       std::cout << item << ' ';
}
template <typename T, typename ...Args>
void printAllImpl(T item, Args ... args) {
       printAllImpl(item);
       printAllImpl(args...);
}
template <typename... Args>
void printAll(Args&&... args) {
       printAllImpl(std::forward<Args>(args) ...);
       std::cout << '\n';
}
int main() {
       printAll(3, 2, 1);
       printAll(8.2, 2, 1.1, "A");
       printAll(23, 32, 8, 11, 9);
}

运行结果: 

3 2 1
8.2 2 1.1 A
23 32 8 11 9

2.参数包的递归解析

可变参数列表中,参数包的展开方式为递归展开,即将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给相同函数递归调用,以此类推,直到参数列表为空。

代码样例:

#include <iostream>
template<typename T, typename... Args>
void show_list(T value, Args... args)
{
       std::cout << value << ", ";
       show_list(args...); //递归调用
}
int main()
{
       int n = 2;
       double m = 3.0;
       std::string str = "test";
       show_list(1, n, m);
       show_list(1, n, m * m, str);
       return 0;
}

以上代码在VS2019中运行时,会报以下编译错误:

“show_list”: 未找到匹配的重载函数
“void show_list(T,Args...)”: 应输入2个参数,却提供了0个

    出现以上问题的原因是,可变参数函数模板通常是递归的。函数在第一次调用时,会使用参数包中的第一个实参,然后递归调用自身来陆续使用参数包中的剩余实参。为了终止递归,我们还需要定义一个非可变参数的函数模板或者普通函数。

    以下代码都包含终止递归的函数模板。

    Demo1: 

#include <iostream>

//用来终止递归并处理参数包中最后一个元素
template<typename T>
void show_list(T value)
{
    std::cout << value << ", ";
}

//参数包中除了最后一个元素之外的其他元素都会调用这个版本的show_list
template<typename T, typename... Args>
void show_list(T value, Args... args)  
{
    std::cout << value << ", ";
    show_list(args...); //递归调用
}

int main()
{
    int n = 2;
    double m = 3.0;
    std::string str = "test";
    show_list(1, n, m);
    show_list(1, n, m * m, str);
    return 0;
}

运行结果:

1, 2, 3, 1, 2, 9, test,

Demo2:

#include <iostream>
void tprintf(const char* format) //终止递归调用
{
    std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs)
{
    for (; *format != '\0'; format++)
    {
        if (*format == '%')
        {
            std::cout << value;
            tprintf(format + 1, Fargs...); //递归调用
            return;
        }
        std::cout << *format;
    }
}
int main()
{
    tprintf("% world% %\n", "Hello", '!', 123);
}

运行结果:

Hello world! 123

特殊情况,当不涉及"typename T"的使用时,可以不需要单独定义一个非可变参数的函数模板来终止递归。

Demo3:

#include <iostream>
using namespace std;
template<typename... Argv>
void print_func(Argv... argv)
{
    cout << "print_func() is called with "
         << sizeof...(Argv)
         << " argument(s)." << endl;
}
int main(void)
{
    print_func();
    print_func(4, "a");
    print_func("a", "b", "c");
    return 0;
}

运行结果:

print_func() is called with 0 argument(s).
print_func() is called with 2 argument(s).
print_func() is called with 3 argument(s).

3.参数包展开过程拆解

演示代码:

#include <iostream>
using namespace std;
void print()
{
       cout << "I am empty.\n";
}
template <typename T, typename... Types>
void print(T var1, Types... var2)
{
       cout << var1 << endl;
       print(var2...);
}
int main()
{
       print(1, 2, 3.14, "test");
       return 0;
}

过程拆解: 

main函数中,第一次调用print,传递的实参:1,参数包剩余元素:2, 3.14, "test"。

第一次递归调用print,传递的实参:2,参数包剩余元素:3.14, "test"

第二次递归调用print,传递的实参:3.14,参数包剩余元素:"test"

第三次递归调用print,传递的实参:"test",参数包中的元素已全部用完。

由于参数包中的元素为空,退出递归,最后调用的是具体函数print()。

运行结果:

1
2
3.14
test
I am empty.

4.sizeof...运算符

    由于带有"typename T"参数的可变参数的模板函数,总是需要再定义一个同名的模板函数或者普通函数来搭配使用,使得代码特别重复。

    为了解决以上问题,可以使用"sizeof..."运算符来保证,在不重复定义同名函数的情况下让递归退出。

    "sizeof..."运算符可以判断参数包中的元素数量。

    退出递归的方式: 判断当参数包的元素个数为零时,退出函数调用。

sizeof...用法演示:

#include <cassert>
#include <stdio.h>
template<class...A>
int func(A...arg) {
    return sizeof...(arg);
}
int main(void) {
    if (func<int>(1, 2, 3, 4, 5) == 5) {
        printf("the num of arg is 5");
    }
    return 0;
}

运行结果: 

the num of arg is 5

sizeof...在结束递归中的使用

Demo1:

#include <iostream>
template<typename T, typename... Args>
void print_2(T value1, Args... args) {
    std::cout << value1 << ", ";
    if(sizeof...(args) > 0) {
        print_2(args...);
    }
}
int main()
{
    print_2(1, 2, "A");
}

以上用法无法导致递归终止,而且还会引起编译报错,原因是if判断无法在该函数模板中生效。为了解决以上问题,C++17标准中引入了编译期if条件判断的表达式"if constexpr"。

Demo2:

#include <iostream>
template<typename T, typename... Args>
void print_2(T value1, Args... args) {
    std::cout << value1 << ", ";
    if constexpr (sizeof...(args) > 0) {
        print_2(args...);
    }
}
int main()
{
    print_2(1, 2, "A");
}

运行结果: 

1, 2, A,

四,参考阅读

《C++17入门经典》

《C++ primer》

《深入理解C++11》

https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/va-arg-va-copy-va-end-va-start?view=msvc-170

https://www.ibm.com/docs/en/zos/2.3.0?topic=lf-va-arg-va-copy-va-end-va-start-access-function-arguments

https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates

https://en.cppreference.com/w/cpp/language/parameter_pack

标签:va,...,void,list,C++,参数,可变
From: https://blog.csdn.net/CoderZZ_2024/article/details/136695400

相关文章

  • C++开发基础——智能指针
    一,智能指针1.智能指针简介智能指针是用法和行为类似于指针的类对象。智能指针的底层对原始指针做了一定的封装。智能指针除了像指针一样可以存储变量的地址,还提供了其他功能,比如可以管理动态内存分配,对引用进行计数等。当智能指针所指向的变量离开了作用域或被重置时,智能......
  • AcWing 1230. K倍区间 C++满分题解
    原题链接https://www.acwing.com/problem/content/1232/题目分析求区间和,我们可以通过前缀和来求出。我们规定sum[i]表示第1个元素到第i个元素的和。那么sum[r]-sum[l-1]就是区间[l,r]的和。一维前缀和for(inti=1;i<=n;i++){scanf("%lld",&sum[i]);......
  • 限流器(流控)+ 线程 C++实现
    在C++中,你可以使用互斥锁(mutex)和条件变量(conditionvariable)来实现一个简单的限流器(流控)以及线程。下面是一个简单的例子,它创建了一个限流器类,该类允许一定数量的线程同时访问某个资源。#include<iostream>#include<thread>#include<mutex>#include<condition_variable>......
  • C++反射
    反射教程让程序看到自己的数据,并且能够对数据进行操作类型萃取对类型做萃取,有一组混合类型,将特定类型获取出来核心思路:使用模板来匹配查找例子:指针类型萃取解除一层指针,三级变二级,二级变一级template<typenameT>structremove_pointer{};template<typenameT>stru......
  • C++版数据结构与算法
    大家好,今天开始给大家每天带来C++版的数据结构与算法,后面也会包括C#的系统学习。这段代码是一个C++实现的排序算法集合。其中包括选择排序(selectionsort)、冒泡排序(bubblesort)、插入排序(insertionsort)和归并排序(mergesort)。算法后越往后越难,此次做这个系列博客,是想从......
  • 23种设计模式核心思想及代码实现(Java C++)
    目录代码OOP七大原则策略模式单例模式观察者模式装饰模式抽象工厂模式工厂模式简单工厂模式工厂模式抽象工厂模式三种工厂模式的区别简单工厂模式和策略模式的不同pipeline模式职责链模式代理模式静态代理动态代理......
  • 使用JMeter从JSON响应的URL参数中提取特定值
    在使用ApacheJMeter进行API测试时,我们经常需要从JSON格式的响应中提取特定字段的值。这可以通过使用JMeter内置的JSON提取器和正则表达式提取器来完成。以下是一个具体的例子,展示了如何从一个JSON响应中提取rowId的值,同时处理字符串终止符。假设我们有以下JSON响应:{"flag":......
  • 语音转文字——sherpa ncnn语音识别离线部署C++实现
    简介Sherpa是一个中文语音识别的项目,使用了PyTorch进行语音识别模型的训练,然后训练好的模型导出成torchscript格式,以便在C++环境中进行推理。尽管PyTorch在CPU和GPU上有良好的支持,但它可能对资源的要求较高,不太适合嵌入式环境或要求轻量级依赖的场景。考虑到模......
  • C++ this指针
    1. this指针的用处一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时......
  • 复试C++16真题_程序设计1_输出句子中每个单词长度
    输入一行文本,按照相应格式输出每个单词的长度#include<iostream>usingnamespacestd;#include<string>#include<vector>#include<iomanip>intmain(){stringsen="qweasdaxszfsfsddwfas";//getline(cin,sen);如果要把输入的空格的记录......