Using variable argument lists and standard C I/O to construct C++ strings

Started by
19 comments, last by exwonder 16 years, 1 month ago
I have a C++ function that looks like this.

std::string LogManager::Message(const char* message, ...)
{
   char buffer[200];
   va_list arg_list;
   va_start(arg_list, message);
   if (vsprintf(buffer, message, arg_list) < 0)
   {
      va_end(arg_list);
      Warning(__FILE__, __LINE__, "invalid formatted string: %s\n", message);
      return;
   }
   va_end(arg_list);

   string msg_string(buffer);
   return msg_string;

Basically all it does is take a printf-like formatted string and a variable argument list and uses that to construct and return a C++ string with all that data embedded. This is all fine and good, except for the temporary char array used in the vsprintf call. I arbitrarily have its size set to 200, but if I get a message argument that is greater than this size, it won't work correctly. That's easy enough to fix though -- I can go through the parameter message and find out exactly how large it is. But that size still isn't large enough, because the user might make a call like: "LogManager::Messages("%s\n", "and this is a really long string that the function is unprepared to handle"); Just looking at the size of the message string, the function would see that it is a size of 5. But we'll need a much, much larger buffer to because of the size of the additional string argument. I could arbitrarily bump up the size of the char array to be the size of the message string plus some arbitrary large number to increase the size, but again this wouldn't be guaranteed to always work. My question is this. Is there a good means in which to ensure that this function will always construct the string correctly, regardless of the size of or number of additional arguments? The only solution I can think of is to open a temporary file, use vfprintf to print the string to that file, and then read the string back from the file and close it. But this is very inefficient for something as basic as what this function is intended to do. Note that I'm -not- looking for an answer that involves using C++ iostreams or the like (I can't use them for this particular application), so please don't just tell me to use C++ libs instead of C ones. Thanks for any advice you can give![smile]

Hero of Allacrost - A free, open-source 2D RPG in development.
Latest release June, 2015 - GameDev annoucement

Advertisement
You can use vsnprintf() to specify the number of characters to write. If the return value indicates that the buffer was too small resize the buffer accordingly and try it again.
Quote:Original post by Roots
I have a C++ function that looks like this.
...
Note that I'm -not- looking for an answer that involves using C++ iostreams or the like (I can't use them for this particular application),


Why not? Especially considering you apparently can use std::string?

Quote:so please don't just tell me to use C++ libs instead of C ones.


Then why are you claiming to write C++?

Thanks SiCrane, that should solve my problem. I wasn't aware of the existence of those functions.


Zahlman: My project requirements dictate that I'm not allowed to use any file or other streams. I don't know the exact reason, but I think it had something to do with streams not being supported in all embedded environments or not being supported in kernel mode on some operating systems. However using C++ strings is allowed.

Hero of Allacrost - A free, open-source 2D RPG in development.
Latest release June, 2015 - GameDev annoucement

I use boost::format for this. It uses string streams internally so it's type-safe and can insert anything with operator<< into a string.

You're going to have a lot of trouble using vsprintf. Type safety is a big issue, there's just no way to know the type of the variables on the stack. You can easily overflow that buffer array. It also can't print any types other than the native types. The boost::format library is far, far superior.

Edit: Missed the thing about not being able to use streams. If there's no streams, there's no type safety, so things get boring and tricky really fast. You should at least be using vsnprintf. That way you can tell if you filled the buffer before all characters were written (compare its return value to the length of your buffer).

On a side note, why can't you just fprintf to the log file? Why do you even need to form the string in the first place? It just seems like an unnecessary step.
Quote:Original post by Roots
Zahlman: My project requirements dictate that I'm not allowed to use any file or other streams. I don't know the exact reason, but I think it had something to do with streams not being supported in all embedded environments or not being supported in kernel mode on some operating systems. However using C++ strings is allowed.


That's a bit odd.

But still, nothing prevents you from learning from the design of the stream interface. It's typesafe and extensible. Given std::string, it shouldn't even be too hard to bang out a cheap imitation of std::stringstream, either.
Quote:Original post by Roots

Zahlman: My project requirements dictate that I'm not allowed to use any file or other streams. I don't know the exact reason, but I think it had something to do with streams not being supported in all embedded environments or not being supported in kernel mode on some operating systems. However using C++ strings is allowed.


std::ostringstream != std::cout.

Unless you're lacking actual definition or implementation, std::ostringstream shouldn't do anything funkier than std::string does. They might even share some common code.

And mixing kernel mode and such arbitrary memory allocation gives me goosebumps. In such dangerous parts, fixed-sized buffers are usually quite excusable. But I digress...
You'll probably want to use alloca if you're going to be making the buffer a variable size. Even then it's a bit tricky or dangerous to be resizing your buffer.

A very very high percentage of professional console game studios use C-style I/O like this for debug prints, logging, and localization. I'm betting most of them just allocate an arbitrary size that "should be large enough" and use vsnprintf to make sure they don't overflow it. If you do it right the resulting string will just be capped.

Then again you're using std::string so you could do multiple vsnprintfs in a loop and concat, but like Antheus stated/implied, the ramifications of that might not be great depending on how and where your routine is called.
FYI: this program is something internal to my company that won't be used or known outside of a group of less than 10 people, so I'm not concerned about security issues like buffer overflowing and such. I'm also against introducing boost into it just for this particular problem, and I'm sure that if I did include it as a requirement to this application my tech lead would make me remove it. And the reason why I am converting it into a string instead of just outputting it to the logfile is because there's actually more to this function that parses and modifies the message string. The string class makes this task much easier than having to parse and modify a char*.


Anyway, here is my solution to the problem. Haven't tested it yet, but hopefully it does the trick.

std::string LogManager::Message(const char* message, ...){   va_list arg_list;   va_start(arg_list, message);   // Use vsnprintf to get the size of the string as it would be rendered   int string_length = vsnprintf(NULL, 0, message, arg_list);   if (string_length <= 0)   {      Warning(__FILE__, __LINE__, "invalid formatted string: %s\n", message);   }      // Now use vsnprintf to construct the string   char buffer[string_length];   if (vsnprintf(buffer, string_length, message, arg_list) < 0)   {      va_end(arg_list);      Warning(__FILE__, __LINE__, "invalid formatted string: %s\n", message);      return;   }   va_end(arg_list);   string msg_string(buffer);   return msg_string;}

Hero of Allacrost - A free, open-source 2D RPG in development.
Latest release June, 2015 - GameDev annoucement

Using a constant that isn't known at compile time as an array size isn't allowed by the C++ standard. Some compilers however will support this. If you want the code to be portable, you could use a std::vector<char> as the buffer.

This topic is closed to new replies.

Advertisement