将可变参数传递给另一个接受可变参数列表的函数

165

我有两个函数,它们都有相似的参数

void example(int a, int b, ...);
void exampleB(int b, ...);

现在example调用exampleB,但是我如何在不修改exampleB的情况下传递可变参数列表中的变量(因为它已经在其他地方使用了)。


6
可能是在C语言中传递可变参数函数的调用的重复问题。 - Greg Hewgill
2
那个问题的解决方案是使用vprintf,但这里不是这种情况。 - Not Available
这与建议的重复内容有关,但绝对不相同:在C中转发可变参数函数的调用? - Jonathan Leffler
请查看此答案:https://dev59.com/QnI_5IYBdhLWcg3wHvU5 - Created
1
这个回答解决了你的问题吗?在C语言中转发可变参数函数的调用 - imz -- Ivan Zakharyaschev
11个回答

164
你不能直接这样做,你必须创建一个接受 va_list 的函数:
#include <stdarg.h>

static void exampleV(int b, va_list args);

void exampleA(int a, int b, ...)    // Renamed for consistency
{
    va_list args;
    do_something(a);                // Use argument a somehow
    va_start(args, b);
    exampleV(b, args);
    va_end(args);
}

void exampleB(int b, ...)
{
    va_list args;
    va_start(args, b);
    exampleV(b, args);
    va_end(args);
}

static void exampleV(int b, va_list args)
{
    ...whatever you planned to have exampleB do...
    ...except it calls neither va_start nor va_end...
}

5
我猜我得做这样的事情,问题是例子函数基本上只是对 vsprintf 的封装,没有太多其他内容 :/ ("wrapper" 可以理解为“封装”或“包装”) - Not Available
@Xeross:请注意,这并不会改变exampleB的外部规范 - 它只是改变了内部实现。我不确定问题出在哪里。 - Jonathan Leffler
第一个参数是否必需,还是所有参数都可以是可变参数? - Qwerty
1
@Qwerty:语法要求函数的第一个参数必须有名称,该函数在签名中带有变量参数列表, ...。如果您已经将可变参数转换为va_list,则可以将va_list传递给另一个只接受va_list的函数,但是该函数(或其调用的函数)必须知道va_list中包含什么内容。 - Jonathan Leffler

99

也许这有点像在池塘中扔石头,但是它似乎可以很好地与C++11可变参数模板一起使用:

#include <stdio.h>

template<typename... Args> void test(const char * f, Args... args) {
  printf(f, args...);
}

int main()
{
  int a = 2;
  test("%s\n", "test");
  test("%s %d %d %p\n", "second test", 2, a, &a);
}

至少它可以与g++一起工作。


我有点困惑 - 这是使用 C++ >= 11 的合法方法吗? - Duncan Jones
@DuncanJones 是的,args...会扩展包。 - Swordfish
1
这是一个函数模板,因此我们最终得到的是多对一的映射,而不是在两个函数之间传递变量参数? - Cauchy Schwarz
13
我简直无法相信找到一个如何使用这个十年前很方便的功能的例子是多么艰难。 - Martin Boros
谢谢。我刚刚使用了这个库以不幸的方式滥用了C++编译器:https://github.com/cubiclesoft/cross-platform-cpp/blob/master/templates/shared_lib.h和https://github.com/cubiclesoft/cross-platform-cpp/blob/master/templates/shared_lib_util.h - CubicleSoft
我该如何获取参数的第一个元素? - Saptak Bhoumik

17

你应该创建那些接受 va_list 参数的函数,并将它们传递进去。可以参考 vprintf 函数作为示例:

int vprintf ( const char * format, va_list arg );

8

我也想包装printf,找到了这里的一个有用的答案:

如何传递可变数量的参数给printf/sprintf

我对性能一点都不感兴趣(我确信这段代码可以在许多方面得到改进,随意进行 :)),这只是为了一般的调试打印,所以我这样做:

//Helper function
std::string osprintf(const char *fmt, ...)
{
    va_list args;
    char buf[1000];
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args );
    va_end(args);
    return buf;
}

然后我可以像这样使用它

Point2d p;

cout << osprintf("Point2d: (%3i, %3i)", p.x, p.y);
instead of for example:
cout << "Point2d: ( " << setw(3) << p.x << ", " << p.y << " )";

c++的输出流在某些方面非常美丽,但实际上,如果你想打印像这样的一些小字符串(如括号、冒号和逗号)插入到数字之间,它们就变得可怕了。


7
一种可能的方法是使用 #define:
#define exampleB(int b, ...)  example(0, b, __VA_ARGS__)

4

可能不是完全相同的情况,但如果您要为字符串格式化函数(例如日志记录器)定义包装器:

void logger(const char *name, const char *format, ...);
void wrapper(const char *format, ...);

当你实现一个调用loggerwrapper时,我们可以首先使用vasprintf创建一个字符串,然后将其传递给logger
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

static void wrapper(const char *format, ...)
{
    char *string;
    va_list args;
    va_start(args, format);

    // variadic printf with allocated string. must free()
    vasprintf(&string, format, args);
    logger("wrapper", "%s", string);

    free(string);
    va_end(args);
}

虽然不是最干净的方法,但可以使用。当你必须避免使用macro函数时,请尝试这个方法。


应该检查 vasprintf 的返回值。 - guan boshen
好的发现。当分配失败时,它会返回-1。 - Jake Fetiu Kim

3

顺便提一下,许多C语言实现都有一个内部的v?printf变体,我认为这应该成为C标准的一部分。具体细节因实现而异,但典型的实现将接受一个包含字符输出函数指针和信息的结构体,以说明应该发生什么。这使得printf、sprintf和fprintf都可以使用相同的“核心”机制。例如,vsprintf可能是这样的:

void s_out(PRINTF_INFO *p_inf, char ch)
{
  (*(p_inf->destptr)++) = ch;
  p_inf->result++;
}

int vsprintf(char *dest, const char *fmt, va_list args)
{
  PRINTF_INFO p_inf;
  p_inf.destptr = dest;
  p_inf.result = 0;
  p_inf.func = s_out;
  core_printf(&p_inf,fmt,args);
}

核心的printf函数会为每个要输出的字符调用p_inf->func; 输出函数可以将字符发送到控制台、文件、字符串或其他地方。如果一个实现暴露了核心printf函数(以及它使用的任何设置机制),那么可以使用各种变化扩展它。

2

这是唯一的方法..也是最好的方法..

static BOOL(__cdecl *OriginalVarArgsFunction)(BYTE variable1, char* format, ...)(0x12345678); //TODO: change address lolz

BOOL __cdecl HookedVarArgsFunction(BYTE variable1, char* format, ...)
{
    BOOL res;

    va_list vl;
    va_start(vl, format);

    // Get variable arguments count from disasm. -2 because of existing 'format', 'variable1'
    uint32_t argCount = *((uint8_t*)_ReturnAddress() + 2) / sizeof(void*) - 2;
    printf("arg count = %d\n", argCount);

    // ((int( __cdecl* )(const char*, ...))&oldCode)(fmt, ...);
    __asm
    {
        mov eax, argCount
        test eax, eax
        je noLoop
        mov edx, vl
        loop1 :
        push dword ptr[edx + eax * 4 - 4]
        sub eax, 1
        jnz loop1
        noLoop :
        push format
        push variable1
        //lea eax, [oldCode] // oldCode - original function pointer
        mov eax, OriginalVarArgsFunction
        call eax
        mov res, eax
        mov eax, argCount
        lea eax, [eax * 4 + 8] //+8 because 2 parameters (format and variable1)
        add esp, eax
    }
    return res;
}

为什么会有人给我点踩呢?这明明是最好的方法,它使用循环将所有参数推入堆栈,然后通过 call eax 调用所有已推入的 push,其中 eaxOriginalVarArgumentsFunction 地址。这样,您就可以使用任意数量的参数来使用 ...,并且仍然能够在钩子中使用它。 - SSpoke
11
你听说过有些处理器不是i386的吗? - Vincent Fourmond
1
为什么会有人钩取 CRT 函数,比如 va_?而且你知道如果使用 /MT 编译,这是无用的吗?另外,你知道在 x64 上没有内联汇编吗?总之...这不是一个好答案。 - Mecanik
无法为随机变量大小的参数生成“...”,这是唯一的方法,没有其他方法可以绕过此问题。我已经测试过了,你不能简单地调用像这样的东西 OriginalVarArgsFunction(variable1, format, ...); 这就是为什么这个修复程序可以解决问题,仅此而已。 - SSpoke

1

使用GNU C扩展:

int FIRST_FUNC(...){
    __builtin_return(
        __builtin_apply(
            (void(*)())SECOND_FUNC, __builtin_apply_args(), 100));
}

1
不支持在MacOS上使用clang,这是否是标准的一部分? - DangerMouse
@DangerMouse 哦,是的,抱歉在三个点之前应该有一个逗号,这使得整个答案都是错误的,因为它不是变量的一部分。 - user20300474

1

根据您包装vsprintf的评论,以及这被标记为C++,我建议不要尝试这样做,而是改变您的接口以使用C++ iostreams。它们比print函数系列具有优势,例如类型安全性和能够打印printf无法处理的项目。现在进行一些重构可能会在未来节省大量痛苦。


你指的是哪些优势? - cjcurrie
@cjcurrie:优点在于类型安全,即使是用户定义的类型也是如此。当然,C函数根本无法处理用户定义的类型。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接