【问题标题】:Are there gotchas using varargs with reference parameters是否有使用带有参考参数的可变参数的陷阱
【发布时间】:2008-10-21 14:58:27
【问题描述】:

我有这段代码(总结)...

AnsiString working(AnsiString format,...)
{
    va_list argptr;
    AnsiString buff;

    va_start(argptr, format);
    buff.vprintf(format.c_str(), argptr);

    va_end(argptr);
    return buff;
}

并且,在可能的情况下,通过引用传递是首选的,因此我对其进行了更改。

AnsiString broken(const AnsiString &format,...)
{
... the rest, totally identical ...
}

我的调用代码是这样的:-

AnsiString s1, s2;
    s1 = working("Hello %s", "World");
    s2 = broken("Hello %s", "World");

但是,s1 包含“Hello World”,而 s2 包含“Hello (null)”。我认为这是由于 va_start 的工作方式,但我不确定发生了什么。

【问题讨论】:

    标签: c++ reference variadic-functions


    【解决方案1】:

    如果您查看 va_start 扩展的内容,您会看到发生了什么:

    va_start(argptr, format); 
    

    变成(大致)

    argptr = (va_list) (&format+1);
    

    如果 format 是一个值类型,它将被放置在所有可变参数之前的堆栈中。如果 format 是引用类型,则只有地址被放入堆栈。当你获取引用变量的地址时,你得到的是地址或原始变量(在这种情况下是在调用 Broken 之前创建的临时 AnsiString),而不是参数的地址。

    如果您不想传递完整的类,您的选择是通过指针传递,或者放入一个虚拟参数:

    AnsiString working_ptr(const AnsiString *format,...)
    {
        ASSERT(format != NULL);
        va_list argptr;
        AnsiString buff;
    
        va_start(argptr, format);
        buff.vprintf(format->c_str(), argptr);
    
        va_end(argptr);
        return buff;
    }
    
    ...
    
    AnsiString format = "Hello %s";
    s1 = working_ptr(&format, "World");
    

    AnsiString working_dummy(const AnsiString &format, int dummy, ...)
    {
        va_list argptr;
        AnsiString buff;
    
        va_start(argptr, dummy);
        buff.vprintf(format.c_str(), argptr);
    
        va_end(argptr);
        return buff;
    }
    
    ...
    
    s1 = working_dummy("Hello %s", 0, "World");
    

    【讨论】:

    • 很好地解释了正在发生的事情。
    • 这是众所周知的泄漏抽象的一个很好的例子。 va_start 看起来像是某种魔法,但实际上它只是一点编译器认可的骇客。
    【解决方案2】:

    以下是 C++ 标准(18.7 - 其他运行时支持)关于 va_start() 的说明(重点是我的):

    ISO C 的限制 的第二个参数 va_start() 标头中的宏 <stdarg.h>在这个不同 国际标准。参数 parmN 是标识符 变量中最右边的参数 函数的参数列表 定义(前一个 ...)。 如果参数parmN 用函数、数组或引用声明 类型, 或使用非 与产生的类型兼容 当传递一个参数时 没有参数,行为 未定义

    正如其他人所提到的,在 C++ 中使用可变参数是危险的,如果你将它与非直接 C 项目一起使用(甚至可能以其他方式)。

    也就是说 - 我仍然一直使用 printf()...

    【讨论】:

    • 那么当 varargs 参数是参数列表中的第一个时会发生什么?
    • @Angelorf:如果变量参数列表左侧没有任何形式参数,则变量参数是不可访问的。这通常没有用,除了少数例外。一个这样的例外是如果您需要与任何参数匹配的函数签名。这通常用作模板元编程中的包罗万象的实现(例如,在匹配通用函数指针类型时)。
    【解决方案3】:

    N0695 中找到了一个很好的分析为什么你不想要这个

    【讨论】:

      【解决方案4】:

      根据 C++ 编码标准(Sutter、Alexandrescu):

      varargs 不应该与 C++ 一起使用:

      它们不是类型安全的,并且对于类类型的对象具有未定义的行为,这可能会导致您的问题。

      【讨论】:

      • 在这种情况下,这不是类类型问题,因为 va_list 中只有 char * 类型。事实上,列表前面的参数是一个引用。
      • 我同意你的回答,关于宏扩展,这是一个很好的观点,可能是这种情况下的实际问题。然而,在你的 C++ 代码中混合它仍然是一个坏主意,因为没有什么可以阻止用户(试图)传递一个 std::string 。
      • 是的,等不及 C++0x 中的可变参数模板了!
      【解决方案5】:

      这是我的简单解决方法(使用 Visual C++ 2010 编译):

      void not_broken(const string& format,...)
      {
        va_list argptr;
        _asm {
          lea eax, [format];
          add eax, 4;
          mov [argptr], eax;
        }
      
        vprintf(format.c_str(), argptr);
      }
      

      【讨论】:

      • 假设 32 位架构与 add eax,4 ?如果是这样,则不是可移植代码。
      【解决方案6】:

      旁注:

      类类型作为可变参数的行为可能是未定义的,但在我的经验中是一致的。编译器将类内存的 sizeof(class) 压入堆栈。即,在伪代码中:

      alloca(sizeof(class));
      memcpy(stack, &instance, sizeof(class);
      

      对于以非常有创意的方式使用它的一个非常有趣的示例,请注意您可以将 CString 实例代替 LPCTSTR 直接传递给 varargs 函数,并且它可以工作,并且有不涉及铸造。我把它作为一个练习留给读者来弄清楚他们是如何做到的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-14
        • 2017-11-28
        • 2020-01-10
        相关资源
        最近更新 更多