c++可变参数和折叠表达式详解

发布于:2025-02-10 ⋅ 阅读:(73) ⋅ 点赞:(0)

目录

引言

 库的基本功能 

va_start 宏:

va_arg 宏

va_end 宏

va_copy 宏

使用  处理可变参数代码

C++11可变参数模板

基本概念

sizeof... 运算符

包扩展

C++17折叠表达式

折叠表达式基本使用举例

可变参模板的缺点:

折叠表达式四种基本语法

一元右折叠

一元左折叠

二元右折叠

二元左折叠

 空包参数注意事项

引言

        在C++编程中,处理不确定数量的参数是一个常见的需求。为了支持这种需求,C标准库提供了 <stdarg.h> 头文件,其中定义了一组宏和类型,用于处理不定参数函数。C++继承了C语言的可变参数机制,使用了stdarg.h提供的宏来处理不确定数量的参数。其原理基于栈的推入和弹出过程,不需要明确参数数量。此外,C++提供了可变参数机制,让我们能够创建接收任意数量参数的函数。这一特性在许多实际应用中非常有用,比如日志记录、函数重载等。

<stdarg.h> 库的基本功能 

<stdarg.h> 库包含以下主要部分:

va_start 宏:

用于初始化 va_list 变量,其基本语法如下:

void va_start(va_list ap, last);
  • apva_list 变量。
  • last:最后一个确定的参数,后面的参数是可变参数。

va_arg 宏

 va_arg 宏用于访问可变参数列表中的下一个参数,其基本语法如下:

type va_arg(va_list ap, type);
  • apva_list 变量。
  • type:要访问的参数的类型。

va_end 宏
 

  va_end 宏用于结束 va_list 变量的访问,其基本语法如下:

void va_end(va_list ap);
  •   ap:va_list 变量。


va_copy 宏

  va_copy 宏用于复制 va_list 变量,其基本语法如下:

void va_copy(va_list dest, va_list src);
  • dest:目标 va_list 变量。
  • src:源 va_list 变量。

使用 <stdarg.h> 处理可变参数代码

        示例中,print_args接收一个格式字符串,然后根据格式字符(i表示整数,d表示双精度浮点数)解析后面的参数。

#include <iostream>
#include <cstdarg>

void print_args(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    
    while (*fmt != '\0') {
        if (*fmt == 'i') {
            int i = va_arg(args, int);
            std::cout << "int: " << i << std::endl;
        } elseif (*fmt == 'd') {
            double d = va_arg(args, double);
            std::cout << "double: " << d << std::endl;
        }
        ++fmt;
    }

    va_end(args);
}

int main()
{
    print_args("ddii", 0.618,3.14, 7, 9);
    return 0;
}
//double: 0.618 double: 3.14 int: 7 int: 9

C++11可变参数模板

基本概念

        C++11通过模板提供了类型安全且灵活的可变参数机制。可以通过递归模板来处理不同类型的参数,避免了手动处理类型的麻烦。也就是支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。

参数包有两种类型:

  • 模板参数包,表示零或多个模板参数,使用class...或typename...关键字声明。
  • 函数参数包,表示零个或多个函数参数,使用类型名后跟...表示。
template<class ...Arg> void Func(Arg... arg) {}
template<class ...Arg> void Func(Arg... arg) {}
template<class ...Arg> void Func(Arg... arg) {}

         我们用省略号...来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表。可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

sizeof... 运算符

sizeof...运算符来计算参数包中参数的个数。

template <class ...Arg>
void PrintArgNum(Arg&&... arg)
{
	cout << sizeof...(arg)<<"个参数包" << endl;
}
 
int main()
{
	double a = 3.14;
 
	PrintArgNum();
	PrintArgNum(a);//一个参数
	PrintArgNum(1, string("241564132"));//两个参数
	return 0;
}

        编译本质会结合引用折叠规则实例化出以下三个函数,在类型泛化基础上叠加了数量变化,让泛型编程更加灵活。

void Print();
void Print(double&& arg1);
void Print(int&& arg1, string&& arg2);

包扩展

        对于一个参数包,我们除了计算它的参数个数,还可以对它进行包扩展。我们还要提供用于每个扩展元素的模式,扩展一个包就是将他分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放省略号(...)来触发扩展操作。

//参数包是0个时,直接匹配这个函数
void ShowList()
{
	cout<< endl;
}
 
template<class T,class ...Arg>
void ShowList(T&& val,Arg&&... arg)
{
	cout << val << endl;
	//arg是N个参数的参数包,调用Printf,参数包的第一个传给val,剩下N-1个传给参数包,
	ShowList(arg...);
}
template <class ...Arg>
void Print(Arg&&... arg)
{
	cout << sizeof...(arg)<<"个参数包" << endl;
	ShowList(arg...);
}
 
int main()
{
	double x = 3.14;
 
	Print();
	Print(11.1);//一个参数
	Print(1, string("bjkbhv"));//两个参数
	Print(12.55, string("9jjug7"), x);//三个参数
	return 0;
}

        实际上是通过递归展开来实现的,当参数包为空时就会调用 void ShowList(),同时终止递归

递归时,T接受传来参数包的第一个参数类型,arg接受其余的参数类型,以此往复。

C++17折叠表达式

折叠表达式基本使用举例

//基础情况 - 只有一个参数时的处理 
template<typename T>
T sum(T v) {
    return v;  // 递归的终止条件
}

//基础情况 - 只有一个参数时的处理 
template<typename T>
T sum(T v) {
    return v;  // 递归的终止条件
}

//C++17 折叠表达式写法
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  
}

编译时实际展开 

sum(1, 2, 3, 4)  
// C++11 可变参数递归版本展开:
1 + sum(2, 3, 4)
1 + (2 + sum(3, 4))
1 + (2 + (3 + sum(4)))
1 + (2 + (3 + 4))

// C++17 折叠表达式直接展开:
((1 + 2) + 3) + 4

可变参模板的缺点:

代码简洁度 : C++11可变参模板需要两个模板函数,而且还要写递归。新版本只需要一个函数,一行代码就搞定。

 编译效率 :可变参数模板在使用时候需要写递归,递归版本每处理一个参数都要生成一次函数调用,而折叠表达式在编译期就能展开成一个扁平的表达式。

运行时性能 : 可变参数模板每个递归调用都会产生函数调用开销,而折叠表达式会被编译器优化成一组简单的加法运算。

折叠表达式四种基本语法

一元右折叠

//一元右折叠 - 从右往左折 
(pack op ...)         
// 例如: (args + ...) 会展开成 a1 + (a2 + (a3 + a4))
template<typename... Args>
void print_right(Args... args) {
    // 从右向左展开: a1 + (a2 + (a3 + a4)) 
    (std::cout << ... << args)  // 从右向左展开
}

一元左折叠

//一元左折叠 - 从左往右折 
(... op pack)         
// 例如: (... + args) 会展开成 ((a1 + a2) + a3) + a4
template<typename... Args>
void print_left(Args... args) {
    // 从左向右展开: ((a1 + a2) + a3) + a4 
    (args << std::cout)  // 从左向右展开
}

二元右折叠

pack op ... op init) 
//例如: (args + ... + 100) 变成 a1 + (a2 + (a3 + 100))
template<typename... Args>
auto sum_right(Args... args) {
    return (args + ... + 100);  // 右边带初始值: a1 + (a2 + (a3 + 100)) 
}

二元左折叠

(init op ... op pack) 
// 例如: (100 + ... + args) 变成 ((100 + a1) + a2) + a3
template<typename... Args>
auto sum_left(Args... args) {
    return (100 + ... + args);  // 左边带初始值: ((100 + a1) + a2) + a3 
}

op 可以用很多运算符

 a. 算术运算符 - 做数学计算用
       
 +, -, *, /, %

b. 位运算符 - 处理二进制位
      
  ^, &, |, <<, >>

c. 赋值运算符 - 存储值用
       
 =, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=

d. 比较运算符 - 判断大小关系
      
  ==, !=, <, >, <=, >=

 e. 逻辑运算符 - 处理真假值
        
&&, ||

f. 其他特殊运算符
        
,(逗号), .*, ->*

 空包参数注意事项

       只有 &&、|| 和逗号运算符才能安全处理空参数包。

template<typename... Args>
bool all(Args... args) {
    return (... && args);    // 安全 
    // return (... + args);  // 危险 
}


网站公告

今日签到

点亮在社区的每一天
去签到