【问题标题】:How to wrap a function with variable length arguments?如何用可变长度参数包装函数?
【发布时间】:2010-09-07 16:05:48
【问题描述】:

我希望在 C/C++ 中执行此操作。

我遇到了Variable Length Arguments,但这表明使用libffi 的Python 和C 解决方案。

现在,如果我想用myprintf 包装printf 函数

我的做法如下:

void myprintf(char* fmt, ...)
{
    va_list args;
    va_start(args,fmt);
    printf(fmt,args);
    va_end(args);
}

int _tmain(int argc, _TCHAR* argv[])
{
    int a = 9;
    int b = 10;
    char v = 'C';
    myprintf("This is a number: %d and \nthis is a character: %c and \n another number: %d\n",a, v, b);
    return 0;
}

但结果并不如预期!

This is a number: 1244780 and
this is a character: h and
another number: 29953463

我错过了什么地方??

【问题讨论】:

  • 现在 C++11 出来了,这个问题的答案非常不同了。
  • @MooingDuck 确实,我添加了一个Variadic templates 答案,您认为C++11 中有更好的方法吗?
  • @MooingDuck 可变参数函数不是可变参数模板函数。它们的性质和类型不同。
  • @rubenvb 在这种情况下,我认为区别并不重要,我看到的几乎所有参考文献都将它们吹捧为可变参数函数的直接替代品:en.cppreference.com/w/cpp/utility/variadic 所以我很想了解区别你在这种情况下看到。
  • @shafik:每个实例化的明显代码膨胀怎么样?传递函数指针怎么样?您需要注意一些区别。我并不是说你不应该,我只是说没有人弃用可变参数函数。

标签: c++ c variadic-functions


【解决方案1】:

问题是您不能将“printf”与 va_args 一起使用。如果您使用可变参数列表,则必须使用 vprintf。 vprint、vsprintf、vfprintf 等(Microsoft 的 C 运行时中也有“安全”版本,可以防止缓冲区溢出等)

您的示例作品如下:

void myprintf(char* fmt, ...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}

int _tmain(int argc, _TCHAR* argv[])
{
    int a = 9;
    int b = 10;
    char v = 'C'; 
    myprintf("This is a number: %d and \nthis is a character: %c and \n another number: %d\n",a, v, b);
    return 0;
}

【讨论】:

  • 您能解释一下为什么“您不能将printfva_args 一起使用”吗?为什么vprintf
  • @JohnStrood 直接回答这个问题:因为 printf() 不接受 'va_list' 作为参数,所以它需要不同数量的参数(例如“...”)。请参阅 printf() 和 vprintf() 的手册页。没有它说 printf() 接受 'va_list' 作为参数,只有可变数量的 % 格式代码期望的类型的参数(int、long、float 等)。只有 vprintf() 系列函数接受 va_list。
  • 1 你必须使用 const char 2 你绝对不能使用 fmt 作为 vprintf 的字符串缓冲区...你测试过这段代码了吗???
【解决方案2】:

在 C++11 中,这是使用 Variadic templates 的一种可能解决方案:

template<typename... Args>
void myprintf(const char* fmt, Args... args )
{
    std::printf( fmt, args... ) ;
}

编辑

正如@rubenvb 指出的那样,需要权衡取舍,例如,您将为每个实例生成代码,这将导致代码膨胀。

【讨论】:

  • 还要注意 printf 和 scanf 系列函数的参数格式检查不适用于模板。不检查格式字符串。如果你得到格式字符串错误它不会在编译时被捕获,但可能会出现段错误(崩溃)或在运行时出现未知行为。
【解决方案3】:

我也不确定你所说的纯是什么意思

在 C++ 中我们使用

#include <cstdarg>
#include <cstdio>

class Foo
{   void Write(const char* pMsg, ...);
};

void Foo::Write( const char* pMsg, ...)
{
    char buffer[4096];
    std::va_list arg;
    va_start(arg, pMsg);
    std::vsnprintf(buffer, 4096, pMsg, arg);
    va_end(arg);
    ...
}

【讨论】:

  • 您可以将编译器属性添加到函数 def 并让编译器检查您的格式参数。 class Foo { attribute ((format (printf, 2, 3))) void Write(const char* pMsg, ...); }; Foo f; f.Write("%s %s %d %s" , "dog" , "cat", "horse", "pig"); “警告:格式指定类型'int',但参数的类型为'const char *' [-Wformat]”
【解决方案4】:

实际上,有一种方法可以从包装器中调用没有va_list 版本的函数。思路是用汇编,不碰栈中的参数,临时替换函数返回地址。

Visual C x86 的示例。 call addr_printf 致电printf()

__declspec( thread ) static void* _tls_ret;

static void __stdcall saveret(void *retaddr) {
    _tls_ret = retaddr;
}

static void* __stdcall _getret() {
    return _tls_ret;
}

__declspec(naked)
static void __stdcall restret_and_return_int(int retval) {
    __asm {
        call _getret
        mov [esp], eax   ; /* replace current retaddr with saved */
        mov eax, [esp+4] ; /* retval */
        ret 4
    }
}

static void __stdcall _dbg_printf_beg(const char *fmt, va_list args) {
    printf("calling printf(\"%s\")\n", fmt);
}

static void __stdcall _dbg_printf_end(int ret) {
    printf("printf() returned %d\n", ret);
}

__declspec(naked)
int dbg_printf(const char *fmt, ...)
{
    static const void *addr_printf = printf;
    /* prolog */
    __asm {
        push ebp
        mov  ebp, esp
        sub  esp, __LOCAL_SIZE
        nop
    }
    {
        va_list args;
        va_start(args, fmt);
        _dbg_printf_beg(fmt, args);
        va_end(args);
    }
    /* epilog */
    __asm {
        mov  esp, ebp
        pop  ebp
    }
    __asm  {
        call saveret
        call addr_printf
        push eax
        push eax
        call _dbg_printf_end
        call restret_and_return_int
    }
}

【讨论】:

  • 我什至不敢写这个,但我不能不佩服它。
【解决方案5】:

您使用的是 C 还是 C++?下一个 C++ 版本 C++0x 将支持 variadic templates,它提供了该问题的解决方案。

另一种解决方法可以通过巧妙的运算符重载来实现如下语法:

void f(varargs va) {
    BOOST_FOREACH(varargs::iterator i, va)
        cout << *i << " ";
}

f(args = 1, 2, 3, "Hello");

为了让它工作,必须实现类varargs 来覆盖operator =,它返回一个代理对象,而代理对象反过来又覆盖operator ,。然而,据我所知,在当前的 C++ 中使这种变体类型安全是不可能的,因为它必须通过类型擦除来工作。

【讨论】:

  • C++03 可以使用boost::tuple,它可以安全地完成上述操作。
【解决方案6】:

纯 C/C++ 解决方案是什么意思?

其余参数 (...) 在 C 运行时支持跨平台。

http://msdn.microsoft.com/en-us/library/kb57fad8.aspx

【讨论】:

    【解决方案7】:
    void myprintf(char* fmt, ...)
    {
        va_ list args;
        va_ start(args,fmt);
        printf(fmt,args); ----> This is the fault. vprintf(fmt, args); should have been used.
        va_ end(args);
    }
    If you're just trying to call printf, 
    there's a printf variant called vprintf that takes 
    the va_list directly :  vprintf(fmt, args);
    

    【讨论】: