使用变量列表参数时的va_list长度?

问题描述 投票:34回答:6

有没有办法计算va_list的长度?我看到的所有示例都明确给出了变量参数的数量。

c++ c
6个回答
29
投票

没有办法计算va_list的长度,这就是为什么你需要printf中的格式字符串就像函数一样。

唯一的 功能 可用于使用va_list are的宏:

  • va_start - 开始使用va_list
  • va_arg - 得到下一个论点
  • va_end - 停止使用va_list
  • va_copy(自C ++ 11和C99) - 复制va_list

请注意,你需要在相同的范围内调用va_startva_end,这意味着你不能将它包装在一个实用程序类中,该实用程序类在其构造函数中调用va_start,在其析构函数中调用va_end(我曾被这个曾经被咬过)。

例如,这个班级毫无价值:

class arg_list {
    va_list vl;
public:
    arg_list(const int& n) { va_start(vl, n); }
    ~arg_list() { va_end(vl); }
    int arg() {
        return static_cast<int>(va_arg(vl, int);
    }
};

海湾合作委员会输出following error

t.cpp:在构造函数arg_list::arg_list(const int&)中: 第7行:错误:va_start用于具有固定args的函数 由于-Wfatal-errors导致编译终止。


11
投票

尚未提及的一种方法是使用预处理器宏来使用va_list长度作为第一个参数来调用variadict函数,并且还沿着参数转发。这有点像“可爱”的解决方案,但不需要手动输入参数列表长度。

假设您具有以下功能:

int Min(int count, ...) {
    va_list args;
    va_start(args, count);

    int min = va_arg(args, int);
    for (int i = 0; i < count-1; ++i) {
      int next = va_arg(args, int);
      min = min < next ? min : next;
    }
    va_end(args);

    return min;
}

这个想法是你有一个预处理器宏,它能够通过使用__VA_ARGS__的掩码来计算参数的数量。有一些很好的预处理器库可用于确定__VA_ARGS__长度,包括P99和Boost预处理器,但我不会在这个答案中留下漏洞,这里是如何做到的:

#define IS_MSVC _MSC_VER && !__INTEL_COMPILER

/**
 * Define the macros to determine variadic argument lengths up to 20 arguments. The MSVC 
 * preprocessor handles variadic arguments a bit differently than the GNU preprocessor,
 * so we account for that here. 
 */
#if IS_MSVC
  #define MSVC_HACK(FUNC, ARGS) FUNC ARGS
  #define APPLY(FUNC, ...) MSVC_HACK(FUNC, (__VA_ARGS__))
  #define VA_LENGTH(...) APPLY(VA_LENGTH_, 0, ## __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#else
  #define VA_LENGTH(...) VA_LENGTH_(0, ## __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#endif

/**
 * Strip the processed arguments to a length variable.
 */
#define VA_LENGTH_(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, N, ...) N

注意:上面的很多噪音都是对MSVC的解决方案。

通过上面定义的,您可以创建一个宏来执行所有基于长度的操作:

/**
 * Use the VA_LENGTH macro to determine the length of the variadict args to
 * pass in as the first parameter, and forward along the arguments after that.
 */
#define ExecVF(Func, ...) Func(VA_LENGTH(__VA_ARGS__), __VA_ARGS__)

只要它以int count参数开头,该宏就能够调用任何variadict函数。简而言之,而不是使用:

int result = Min(5, 1, 2, 3, 4, 5);

您可以使用:

int result = ExecVF(Min, 1, 2, 3, 4, 5);

这是Min的模板版本,使用相同的方法:https://gist.github.com/mbolt35/4e60da5aaec94dcd39ca


10
投票

变量函数没有直接的方法来确定传递了多少个参数。 (至少没有可移植的方式; <stdarg.h>接口不提供该信息。)

有几种间接方式。

两个最常见的是:

  • 格式字符串(通过您可能称之为小型简单语言的字符串,指定剩余参数的数量和类型)。 *printf()*scanf()函数族使用这种机制。
  • 表示参数结束的sentinel值。一些Unix / POSIX exec*()函数系列使用空指针标记参数的结尾。

但还有其他可能性:

  • 更简单地说,一个整数计数,指定后续参数的数量;大概在这种情况下,他们都属于同一类型。
  • 交替参数,其中参数可以是指定以下参数类型的枚举值。一个假设的例子可能如下: func(ARG_INT, 42, ARG_STRING, "foo", ARG_DOUBLE, 1.25, ARG_END); 甚至: func("-i", 42, "-s", "foo", "-d", 1.25, ""); 如果你想模仿参数通常传递给Unix命令的方式。

您甚至可以为全局变量赋值以指定参数的数量:

func_arg_count = 3;
func(1, 2, 3);

这将是丑陋但完全合法的。

在所有这些技术中,调用者完全有责任传递一致的参数;被调用者只能假设其参数是正确的。

请注意,处理传递给它的所有参数不需要可变参数函数。例如,这个:

printf("%d\n", 10, 20);

将打印10并静静地忽略20。几乎没有任何理由可以利用该功能。


2
投票

如果您在MS Visual Studio下工作,可以尝试使用函数_vscprintf。这是一个如何使用_vscprintf的例子,我用它来知道我的控制台标题需要多少空间来malloc。

int SetTitle(const char *format,...){
    char *string;
    va_list arguments;

    va_start(arguments,format);
        string=(char *)malloc(sizeof(char)*(_vscprintf(format,arguments)+1));
        if(string==NULL)
            SetConsoleTitle("Untitled");
        else
            vsprintf(string,format,arguments);
    va_end(arguments);

    if(string==NULL)
        return SETTITLE_MALLOCFAILED;
    SetConsoleTitle(string);
    free(string);
    return 0;
}

或者你可以这样做,将输出添加到临时文件,然后从中读取数据到分配的内存,就像我在下一个例子中所做的那样:

void r_text(const char *format, ...){
    FILE *tmp = tmpfile();
    va_list vl;
    int len;
    char *str;

    va_start(vl, format);
        len = vfprintf(tmp, format, vl);
    va_end(vl);
    rewind(tmp);
    str = (char *) malloc(sizeof(char) * len +1);
    fgets(str, len+1, tmp);
    printf("%s",str);
    free(str);
    fclose(tmp);
}

2
投票

嗯,如果你不害怕讨厌的asm hack那么你可以利用你的编译器的调用约定。但是,这会将您的代码限制为特定的平台/编译器/调用约定。

例如,在BDS2006 C ++ 32位x86 Windows应用程序(我将仅参考此平台)中,将参数放入堆栈然后调用,然后在返回函数后修复堆栈指针值(通过使用堆栈的大小)。这里的小例子:

double x;
x=min(10.0,20.0,30.0,40.0,50.0);

电话转换为:

Unit1.cpp.28: x=min(10.0,20.0,30.0,40.0,50.0);
00401B9C 6800004940       push $40490000
00401BA1 6A00             push $00
00401BA3 6800004440       push $40440000
00401BA8 6A00             push $00
00401BAA 6800003E40       push $403e0000
00401BAF 6A00             push $00
00401BB1 6800003440       push $40340000
00401BB6 6A00             push $00
00401BB8 6800002440       push $40240000
00401BBD 6A00             push $00
00401BBF E894FDFFFF       call min(double,double,????)
00401BC4 83C428           add esp,$28

注意通话后的最后一条指令。 $28是由4个参数和一个返回值消耗的大小。因此,如果您可以在函数中读取该值,则可以确切地确定参数的数量(如果它们的大小已知)。所以这里的工作示例:

double min(double x,double ...) // = min(x,y)
        {
        int n,dn=sizeof(double);
        asm {
            mov eax,esp // store original stack pointer 
            mov esp,ebp // get to the parrent scope stack pointer
            pop ebx
            pop ebx     // this reads the return address of the call pointing to the first instruction after it which is what we want
            mov esp,eax // restore stack pointer
            sub eax,eax; // just eax=0
            mov al,[ebx+2] // read lowest BYTE of eax with the $28 from the add esp,$28
            mov n,eax // store result to local variable for usage
            }
        n-=dn;  // remove return value  from the count

        double z; z=x;
        va_list va;
        va_start(va,x); n-=dn;
        for (;n>=0;n-=dn)
            {
            x=va_arg(va,double);
            if (z>x) z=x;
            }
        va_end(va);
        return z;
        }

注意每个编译器可以有不同的调用顺序,所以在使用前先调试汇编列表!


-2
投票

使用_vscprintf确定变量列表的长度。 https://msdn.microsoft.com/en-us/library/w05tbk72.aspx

© www.soinside.com 2019 - 2024. All rights reserved.