32

I have this piece of code (summarized)...

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

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

    va_end(argptr);
    return buff;
}

And, on the basis that pass by reference is preferred where possible, I changed it thusly.

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

My calling code is like this:

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

I think this is due to the way va_start works, but I'm not exactly sure what's going on.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
Roddy
  • 66,617
  • 42
  • 165
  • 277

6 Answers6

43

If you look at what va_start expands out to, you'll see what's happening:

va_start(argptr, format); 

becomes (roughly)

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

If format is a value-type, it gets placed on the stack right before all the variadic arguments. If format is a reference type, only the address gets placed on the stack. When you take the address of the reference variable, you get the address or the original variable (in this case of a temporary AnsiString created before calling Broken), not the address of the argument.

If you don't want to pass around full classes, your options are to either pass by pointer, or put in a dummy argument:

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");

or

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");
BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
Eclipse
  • 44,851
  • 20
  • 112
  • 171
15

Here's what the C++ standard, [support.runtime]/[cstdarg.syn] p1.2 says about va_start() (emphasis mine):

The restrictions that ISO C places on the second parameter to the va_start() macro in header <stdarg.h> are different in this International Standard. The parameter parmN is the identifier of the rightmost parameter in the variable parameter list of the function definition (the one just before the ...). If the parameter parmN is declared with a function, array, or reference type, or with a type that is not compatible with the type that results when passing an argument for which there is no parameter, the behavior is undefined.

As others have mentioned, using va_list in C++ is dangerous if you use it with non-straight-C items (and possibly even in other ways).

That said - I still use printf() all the time...

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
Michael Burr
  • 333,147
  • 50
  • 533
  • 760
  • So what would happen when the varargs argument is the first in the parameter list? – Tim Kuipers Oct 15 '15 at 09:04
  • @Angelorf: If there aren't any formal parameters to the left of the variable argument list, the variable arguments are inaccessible. This is not generally useful, with few exceptions. One such exception is if you need a function signature that matches any argument. This is commonly used as a catch-all implementation in template meta-programming (e.g. when matching generic function pointer types). – IInspectable Apr 28 '16 at 10:34
4

A good analysis why you don't want this is found in N0695 Proposed Changes to C++ <stdarg>

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
MSalters
  • 173,980
  • 10
  • 155
  • 350
1

Here's my easy workaround (compiled with 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);
}
York
  • 2,056
  • 1
  • 15
  • 23
1

According to C++ Coding Standards (Sutter, Alexandrescu):

varargs should never be used with C++:

They are not type safe and have UNDEFINED behavior for objects of class type, which is likely causing your problem.

lefticus
  • 3,346
  • 2
  • 24
  • 28
  • In this case, it's not the class type issue, since there are only char * types in the va_list. It's the fact that the argument preceding the list is a reference. – Eclipse Oct 21 '08 at 15:29
  • 1
    I agree with your answer, about the macro expansion, it's a good point and probably the actual problem in this case. However, it's still a bad idea to mix it up in your C++ code, as there is nothing to prevent a user from (trying to) pass a std::string in. – lefticus Oct 21 '08 at 15:33
  • Yup, can't wait for variadic template in C++0x! – Eclipse Oct 21 '08 at 15:36
-2

Side note:

The behavior for class types as varargs arguments may be undefined, but it's consistent in my experience. The compiler pushes sizeof(class) of the class's memory onto the stack. Ie, in pseudo-code:

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

For a really interesting example of this being utilized in a very creative way, notice that you can pass a CString instance in place of a LPCTSTR to a varargs function directly, and it works, and there's no casting involved. I leave it as an exercise to the reader to figure out how they made that work.

Nick
  • 6,808
  • 1
  • 22
  • 34