C/C++:变长参数技巧汇总

C/C++常见的变长参数技巧包括变长模板、变长函数参数和变长宏参数。

变参数函数

最常见的变参数函数就是printfscanf之类的,利用stdarg.h对变参数函数支持实现的变参函数。其基本思路是根据格式串来判断后面的参数类别,例如读到%d,那么下一个参数就是int/long int,且按十进制整数的方式处理。
下面是一个丐版printf函数的示例,只处理了单字符、单字符串、双精度浮点数和十进制整数4种情况。实际上使用的printf很复杂,在glibc中的vfprintf函数就有2300行源码,包括了对各类基本数据类型输出的支持,还有对齐、填充、浮点样式等支持。

#include<stdio.h>
#include<stdarg.h>

void myprintf(const char* fmt, ...) {
  int symf = 0;

  va_list args;
  va_start(args, fmt);

  int va_int;
  double va_flt;
  const char* va_str;
  char va_chr;
  while (*fmt != '\0') {
    if (symf) {
      if (*fmt == 'c')
        va_chr = va_arg(args, char), printf("%c", va_chr);
      else if (*fmt == 's')
        va_str = va_arg(args, const char*), printf("%s", va_str);
      else if (*fmt == 'd')
        va_int = va_arg(args, int), printf("%d", va_int);
      else if (*fmt == 'f')
        va_flt = va_arg(args, double), printf("%lf", va_flt);
      symf = 0;
    } else {
      if (*fmt == '%')
        symf = 1;
      else
        putchar(*fmt);
    }
    fmt++;
  }
  putchar('\n');

  va_end(args);
}

通常的变参函数都有如下格式:

int my_variadic_func(int argc, ...) {
  // initialize
  va_list args;
  va_start(args, argc);
  int res = 0;
  /* deal with variadic arguments */
  va_end(args);
  // return the answer
  return res;
}

C23标准前的变参函数支持要求,所有变参函数都至少有一个不变参数,从这个参数开始,所有参数视作变参数。
例如:

int printf(const char* fmt, ...);

void foo()
{
  ...
  printf("%d %d %d\n", 2, 3, 5);
  ...
}

printf的定义中,fmt作为不变参数,其后的所有参数视作变参数,包括将被按格式输出的各个表达式参数。
变参函数如何访问和使用变参数?va_list结构是C语言标准中用于支持变参函数访问的结构,它允许用户用va_start宏设置变参数的起始位置,用va_arg逐个访问变参数,用va_end结束对变参数的访问。
在上面的变参函数格式my_variadic_func中,va_list args创建了一个变参列表结构,下一行的va_start(args, argc)指定变参数从argc的下一个参数开始,而va_end(args)表示对变参数的访问结束。
至于中间访问变参数,要使用va_arg宏,它的格式是va_arg(vl, type)vl表示被访问的变参列表,type表示想取的参数类型。具体而言,是调用一次就取走一个参数,下一次调用就取走下一个。注意,取走的参数数量没有明确限制,如果超出变参范围继续取,就会越界访问参数后面的用户栈,所以必须在实现函数时做好约定,让调用者正确地调用函数,在函数实现中取走正确个数的参数,否则会造成程序混乱。printf等输出函数是通过fmt串约定的变参个数,具体实现上,你也可以通过一个整数参数表示变参个数。
完善上面的my_variadic_func,使其成为一个求和函数:

int my_variadic_func(int argc, ...) {
  va_list args;
  va_start(args, argc);
  int res = 0, x;
  while (argc--) {
    x = va_arg(args, int);
    res += x;
  }
  va_end(args);
  return res;
}

另外,va_copy支持完全复制一个现有的变参列表,在变参函数传参时会很有用。详情参考:C++ Reference: va_copy

变参数宏

变参数宏是指支持可变个数参数的文本替换宏#define,这个预处理语法最早在C++11被支持。它允许使用__VA_ARGS__访问参数列表里...所指的内容,也允许将变参数加前缀命名,例如args...etc...,也可以起到__VA_ARGS__一样的效果。

#define myprintf(...)                                                  \
  do {                                                                 \
    time_t lt = time(0);                                               \
    tm* localt = localtime(&lt);                                       \
    printf("[%04d-%02d-%02d %02d:%02d:%02d] ", localt->tm_year + 1900, \
           localt->tm_mon + 1, localt->tm_mday, localt->tm_hour,       \
           localt->tm_min, localt->tm_sec);                            \
    printf("[%12s:%4d] ", __FILE__, __LINE__);                         \
    printf(__VA_ARGS__);                                               \
    putchar('\n');                                                     \
  } while (0);

int main() {
  myprintf("ALOHA");
  return 0;
}

上面是一个按日志格式输出格式化文本的示例。因为我不太懂怎么在两个变参数函数之间传变参,所以我一般用宏的方式处理。
还有一个需要注意的点就是,对于带定参数的可变参数宏(例如macprintf(fmt,...)),可能出现因为没传可变参数导致__VA_ARGS__实际为空,间接导致编译错误的问题。在C++20之前的GCC编译器下,解决方法是用##预处理运算符,将预处理定义修改成诸如下面的格式:

#define mpr(fmt,...) printf(fmt, ##__VA_ARGS__);
#define mpr(fmt,args...) printf(fmt, ##args);

从GNU C++20开始,__VA_OPT__(x)用于在__VA_ARGS__非空时表示内容x,若__VA_ARGS__为空,则不具备意义。

#if __cplusplus >= 202002L // C++20
#define mpr(fmt,...) printf(fmt __VA_OPT__(,) __VA_ARGS__);
#else
// mpr(fmt,...) of older versions than C++20
#endif

这个定义和上面两个定义是等价的。
注意:对于C++中的宏,原则仍然是能不用就不用,只有在其对降低工作量的效果远胜于其潜在出错成本时才可以用。如果一定要使用基于文本替换的C++宏,若替换内容为表达式,在外加一层圆括号,如果替换内容为语句或语句组,则用一个do-while语句块将其包裹起来。

可变参数模板

虽然我比较讨厌C++模板编程的一些细节,但作为学生还是有机会学的都学一下子。
我平时做算法题经常使用的一个东西就是debug函数,可以不写老臭的#ifndef ONLINE_JUDGE,提交之前将函数体内注释掉就可以。

void debug(){ cout<<endl; }
template<class T1,class... T2>
void debug(T1 a,T2... oth)
{
  cout<<a<<' ';
  debug(oth...);
}

上面的debug接受任意个数参数并将这些参数输出到标准输出流。准确来讲,这种语法叫做形参包,是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。
C++20之前,支持这三类形参包作为模板形参出现。C++20开始出现带类型约束的模板形参包,这里不做扩展。

// 1 - fixed type
int... args
// 2 - variadic types
class... args
typename... args
// 3 - nested packages
template</* arguments of nested package */>... args

最常用的是第二类,即类型模板形参包,可匹配0或多个模板实参,且对实参类型限制较少。
形参包最常见的作用,就是为模板提供扩充功能,使其不限于固定个数模板参数,这提供了另一种变参数函数的实现方法,以及使得基于可变模板的一些STL黑科技成为可能。
下面是一个利用形参包特性求和的类模板(实际不可能这么写,这个只能实现静态运算,这里只是举个栗子):

template<int arg0 = 0, int... args>
class Sum
{
 public:
  int operator()() {
  	if(sizeof...(args) == 0) return arg0;
  	else return arg0 + Sum<args...>()();
  }
};

具体而言,带形参包的函数模板接受任意不少于固定形参个数的入口参数,上面的debug由于额外重载了空参,可以接受任意多的参数;带形参包的类模板则接受任意不少于固定形参个数的模板实参,像上面的Sum就可以接受任意多个数的int常量。

形参包的一个重要操作是包的展开。包展开依托一个模式,模式则是包含至少一个形参包的类模板,例如我们的形参包为typename... args,则argsstd::vector<args>std::pair<args,int>均为合法模式。如果包含两个及以上形参包,则所有形参包必须等长。这个很绕的例子大概可以说明这个特性:

template <typename... args>
class TypeArray {};
template <typename A, typename B>
class TypePair {};
template <typename... args1>
class TypeMatcher {
 public:
  template <typename... args2>
  class Accepts {
   public:
    using result_type = TypeArray<TypePair<args1, args2>...>;
  };
};

// sizeof...(args1) == sizeof...(args2) ===> OK
using T1 = TypeMatcher<int, short>::Accepts<size_t, char16_t>::result_type;
// Compilation error: mismatched argument pack lengths while expanding 'TypePair<args1, args2>'
using T2 = TypeMatcher<int, short>::Accepts<std::string>::result_type;

在模式合法的前提下,包展开的格式为:

pattern...

形参包会原地展开为包内各实参代入模式后的结果序列。例如TypeArray<args>...args=[int,int64_t,__int128_t]展开后即为TypeArray<int>,TypeArray<int64_t>,TypeArray<__int128_t>,而不是有些人可能设想的TypeArray<int,int64_t,__int128_t>。后者实际上是TypeArray<args...>的展开结果。

谨慎在相近位置使用多个包展开。包展开运算具有比较低(低于绝大多数运算,不知道是不是最低)的优先级,所以会在一些情形下造成反常识的展开结果。例如下面的例子:

template<class... args> int h(args... a) { return sizeof...(args); }

template<class... Args>
int f(Args... args)
{
  h(h(args...)+args...);
}

f(1,2,3);

很多人会认为前后两个都是单独对args展开,但实际不是。前一个args...展开为1,2,3没有问题,问题在于编译器会认为第一次展开后的h(1,2,3)+args是一个模式,而不是第二个args自身。所以实际展开结果是:h(1,2,3)+1,h(1,2,3)+2,h(1,2,3)+3,而非:h(1,2,3)+1,2,3

另外,ISO C++也对允许包展开的情形进行了十分严格的限制,详情参考:包展开的场所

目前C++官方提供了这三种变长参数支持。在它们之间选择应当加以权衡,选择最适合的方法。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,我之前的回答有误。在VSCode中,没有名为"C/C++: Edit Configurations"的具体插件或设置。请忽略我之前的错误信息。 对于C/C++开发环境的配置,你可以按照以下步骤进行: 1. 确保已安装C/C++插件:在VSCode中,点击左侧导航栏中的扩展按钮(或按下`Ctrl+Shift+X`快捷键)打开扩展面板。在搜索框中输入"C/C++",找到并安装"Microsoft C/C++"插件。 2. 配置编译器和头文件路径:在VSCode中,点击左侧导航栏中的文件夹图标打开文件资源管理器。打开你的C/C++项目文件夹,并在该文件夹中创建一个名为`.vscode`的文件夹(如果还没有)。在`.vscode`文件夹中创建一个名为`c_cpp_properties.json`的文件,并在其中添加以下内容: ```json { "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**" ], "defines": [], "compilerPath": "YOUR_COMPILER_PATH", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "gcc-x64" } ], "version": 4 } ``` 确保将`YOUR_COMPILER_PATH`替换为你实际使用的编译器路径。 3. 配置任务(可选):在VSCode中,按下`Ctrl+Shift+P`快捷键,打开命令面板。输入"Tasks: Configure Default Build Task"并选择该选项。然后选择你使用的编译器,例如"g++"或"clang++"。这将在项目根目录下创建一个名为`tasks.json`的文件,并在其中定义默认的构建任务。 以上步骤可以帮助你配置C/C++开发环境并使用VSCode进行编码和调试。祝你编程愉快!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值