18

A previous question showed a nice way of printing to a string. The answer involved va_copy:

std::string format (const char *fmt, ...);
{
   va_list ap;
   va_start (ap, fmt);
   std::string buf = vformat (fmt, ap);
   va_end (ap);
   return buf;
}


std::string vformat (const char *fmt, va_list ap)
{
   // Allocate a buffer on the stack that's big enough for us almost
   // all the time.
   s ize_t size = 1024;
   char buf[size];

   // Try to vsnprintf into our buffer.
   va_list apcopy;
   va_copy (apcopy, ap);
   int needed = vsnprintf (&buf[0], size, fmt, ap);

   if (needed <= size) {
       // It fit fine the first time, we're done.
       return std::string (&buf[0]);
   } else {
       // vsnprintf reported that it wanted to write more characters
       // than we allotted.  So do a malloc of the right size and try again.
       // This doesn't happen very often if we chose our initial size
       // well.
       std::vector <char> buf;
       size = needed;
       buf.resize (size);
       needed = vsnprintf (&buf[0], size, fmt, apcopy);
       return std::string (&buf[0]);
   }

}

The problem I'm having is that the above code doesn't port to Visual C++ because it doesn't provide va_copy (or even __va_copy). So, does anyone know how to safely port the above code? Presumably, I need to do a va_copy copy because vsnprintf destructively modifies the passed va_list.

Community
  • 1
  • 1
user48956
  • 14,850
  • 19
  • 93
  • 154
  • I've implemented similar things in VC++ and have never needed to use `va_copy()`. What happens when you try it without using the copy? – Greg Hewgill Feb 17 '09 at 18:57
  • 3
    Who knows... It may appear to work. Even if it does, it doesn't mean its safe. – user48956 Feb 17 '09 at 19:07
  • 1
    Apparently va_copy() is a C99 thing. For VC++, you will be just fine using the original va_list more than once without worrying about a copy. vsnprintf won't try to modify the passed list. – Greg Hewgill Feb 17 '09 at 19:50
  • 1
    Practically speaking, copying it without va_copy is fine because a va_list in Visual C++ is just a pointer to the next argument, and calling vsnprintf can't modify the caller's value. Technically, though, ap's value after the first call is indeterminate, so you shouldn't use it for a second call. – Rob Kennedy Feb 17 '09 at 20:05
  • It's also worth mentioning here that Microsoft's implementation of vsnprintf returns -1 if the buffer isn't big enough for the output, with one exception: if the NULL doesn't fit, it'll give you back the buffer filled and the number of characters written just (obviously, perhaps) without the NULL. (I'm not sure how to get the required buffer size via the MS API; eastl just iteratively hunts for it.) – dash-tom-bang Jan 03 '14 at 03:31

4 Answers4

14

You should be able to get away with just doing a regular assignment:

va_list apcopy = ap;

It's technically non-portable and undefined behavior, but it will work with most compilers and architectures. In the x86 calling convention, va_lists are just pointers into the stack and are safe to copy.

Adam Rosenfield
  • 390,455
  • 97
  • 512
  • 589
  • 1
    True - va_copy is commonly defined as "#define va_copy(d,s) ((d) = (s))", so it might be best just to throw that into a 'portability' header protected by an "#ifndef va_copy" – Michael Burr Feb 17 '09 at 19:34
  • 1
    This breaks on AMD64 with GCC. – Alex B Dec 16 '10 at 23:26
  • 2
    @Alex B: Then use `va_copy`, which GCC supports. This question was specifically about Visual C++. – Adam Rosenfield Dec 17 '10 at 05:43
  • yes, that's one may do to achieve reasonable portability. However this breaks on MSVC, so you are back to ifdefs. Oh well. :( – Alex B Dec 17 '10 at 16:09
  • With MSVC on win32 and xbox, I neither knew about nor needed va_copy. I just ran into a toolchain that _did_ require va_copy though, so I'm glad you posted this question. – Paul Du Bois Jul 25 '12 at 04:04
  • This change doesn't compile on most of platforms in contrast to as mentioned in "it will work on most compilers". So, yes it is non-portable code perhaps works in msvc. – irsis May 11 '15 at 11:03
9

For Windows, you can simply define va_copy yourself:

#define va_copy(dest, src) (dest = src)
Steve K
  • 643
  • 6
  • 8
  • I would go with this solution if va_copy is not already defined. MSVC only defines it for 2013. A simple assignment will work if the implementation is a stack pointer which is what msvc does, but will cause problems in gcc and clang with a 64bit architecture. See http://www.bailopan.net/blog/?p=30 – brita_ Feb 04 '16 at 09:56
5

One thing you can do is if you do not otherwise need the vformat() function, move its implementation into the format() function (untested):

#include <stdarg.h>
#include <string.h>
#include <assert.h>
#include <string>
#include <vector>


std::string format(const char *fmt, ...)
{
   va_list ap;

   enum {size = 1024};

   // if you want a buffer on the stack for the 99% of the time case 
   //   for efficiency or whatever), I suggest something like
   //   STLSoft's auto_buffer<> template.
   //
   //   http://www.synesis.com.au/software/stlsoft/doc-1.9/classstlsoft_1_1auto__buffer.html
   //
   std::vector<char> buf( size);

   //
   // where you get a proper vsnprintf() for MSVC is another problem
   // maybe look at http://www.jhweiss.de/software/snprintf.html
   //

   // note that vsnprintf() might use the passed ap with the 
   //   va_arg() macro.  This would invalidate ap here, so we 
   //   we va_end() it here, and have to redo the va_start()
   //   if we want to use it again. From the C standard:
   //
   //       The object ap may be passed as an argument to
   //       another function; if that function invokes the 
   //       va_arg macro with parameter ap, the value of ap 
   //       in the calling function is indeterminate and 
   //       shall be passed to the va_end macro prior to 
   //       any further reference to ap.   
   //
   //    Thanks to Rob Kennedy for pointing that out.
   //
   va_start (ap, fmt);
   int needed = vsnprintf (&buf[0], buf.size(), fmt, ap);
   va_end( ap);

   if (needed >= size) {
       // vsnprintf reported that it wanted to write more characters
       // than we allotted.  So do a malloc of the right size and try again.
       // This doesn't happen very often if we chose our initial size
       // well.
       buf.resize( needed + 1);

       va_start (ap, fmt);
       needed = vsnprintf (&buf[0], buf.size(), fmt, ap);
       va_end( ap);

       assert( needed < buf.size());
   }

   return std::string( &buf[0]);
}
Michael Burr
  • 333,147
  • 50
  • 533
  • 760
3

va_copy() is directly supported starting in Visual Studio 2013. So if you can rely on that being available, you don't need to do anything.

thakis
  • 5,405
  • 1
  • 33
  • 33