如何使用可变长度参数包装一个函数?

51

我希望在C/C++中实现这个功能。我找到了可变长度参数,但是这个文档提供的解决方案涉及Python和C,使用了libffi

现在,如果我想要用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

我错过了什么?


2
现在C++11已经发布,这个问题的答案非常不同。 - Mooing Duck
@MooingDuck,我添加了一个Variadic templates的答案,你认为在C++11中有更好的方法吗? - Shafik Yaghmour
@MooingDuck 一个可变参数函数不是一个可变参数模板函数。它们在本质和类型上都是不同的。 - rubenvb
1
@rubenvb 在这种情况下我认为区别并不重要,我看到的几乎所有参考资料都将它们标榜为可变参数函数的直接替代品:http://en.cppreference.com/w/cpp/utility/variadic 因此我很想了解你在这种情况下看到的区别。 - Shafik Yaghmour
1
@shafik:每个实例化的明显代码膨胀怎么办?传递函数指针怎么样?你需要注意一些区别。我不是说你不应该这样做,我只是说没有人废弃可变参数函数。 - rubenvb
显示剩余5条评论
7个回答

69

问题在于无法使用“printf”和va_args。如果您使用可变参数列表,则必须使用vprintf。还有vprintvsprintfvfprintf等(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 - John Strood
1
@JohnStrood 直接回答你的问题:因为 printf() 不接受 'va_list' 作为参数,它期望一个可变数量的参数(例如 "..."),这是不同的。请参阅 printf() 和 vprintf() 的 man 手册。没有地方说 printf() 接受 'va_list' 作为参数,只有 % 格式代码所期望的类型的可变数量的参数(int、long、float 等)。只有 vprintf() 函数族接受 va_list。 - erco
  1. 你必须使用const char。
  2. 你绝对不能将fmt用作vprintf的字符串缓冲区...你测试过这段代码吗?
- user7082181

18

C++11 中,使用可变参数模板是一种可能的解决方案:

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

正如rubenvb所指出的,需要考虑权衡。例如,每个实例都会生成代码,这将导致代码膨胀。


还要注意的是,printf和scanf系列函数的参数格式检查无法与模板一起使用。格式字符串不受检查。如果您在格式字符串中出现错误,它不会在编译时被捕获,但可能会在运行时崩溃或表现出未知的行为。 - Chris Reid
我使用nios2-elf-gcc编译器进行了测试,效果非常好! - Andak

8
我也不确定您所说的“纯粹”是什么意思。
在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);
    ...
}

你可以在函数定义中添加编译器属性,并让编译器检查你的格式参数。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]" - Chris Reid

7

实际上,有一种方法可以从包装器中调用没有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
    }
}

6
我甚至不敢写下这句话,但我无法不钦佩它。 - Francesco Dondi

1
你正在使用C语言还是C++语言?下一个版本的C++,即C++0x,将支持可变参数模板,这将提供解决该问题的方案。
通过巧妙地进行运算符重载,可以实现如下所示的语法,从而实现另一种解决方法:
void f(varargs va) {
    BOOST_FOREACH(varargs::iterator i, va)
        cout << *i << " ";
}

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

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

1
C++03 可以使用 boost::tuple,它有安全地执行上述操作的方法。 - Mooing Duck

0
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);
}

如果你只是想调用 printf,那么有一个名为 vprintf 的变体可以直接使用 va_listvprintf(fmt, args);


0

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