12

I'm trying to create a function that takes a variable amount of std::string arguments and formats a string with it.

Example:

Test::formatLine(const string::format, ...)
{
    const std::string buffer;
va_list args;
va_start(args, format);
vsprintf(buffer.c_str, format.c_str, args);
va_end(args);
cout << buffer << endl;
}

Compiling this snippet errors:

Error   1   error C3867: 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str': function call missing argument list; use '&std::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str' to create a pointer to member

What I want to achieve:

Test t = Test();
t.formatLine("Hello %s!", "monsieur");

should print Hello monsieur!

t.formatLine("Hello %s %s! How %s you today?", "good", "sir", "are");

should print Hello good sir! How are you today?

Is it even possible to use va_list and vsprintf with std::string only, avoiding char buffer[size]?

Working example (so far) with fixes suggested by Igor, using buffer:

void Test::formatLine(string format, ...)
{
    char buffer[256];
    va_list args;
    va_start(args, format);
    vsprintf_s(buffer, format.c_str(), args);
    va_end(args);
    cout << buffer << endl;
}

Using Igor Tandetnik's suggestion and sample code I finally got a working example that does not use char buffer[size]:

void Test::formatLine(string format, ...)
{
    vector<char> buf(256);
    va_list args;
    va_start(args, format);
    vsnprintf_s(&buf[0], buf.size(), buf.size() + strlen(format.c_str()), format.c_str(), args);
    va_end(args);
    cout << &buf[0] << endl;
}
phew
  • 808
  • 1
  • 15
  • 34
  • 3
    Only trivial types are allowed to be passed to `...`, `std::string` is no such type. – Xeo Sep 25 '13 at 16:00
  • @Xeo But he is only passing trivial types in his examples :) Of course, you're right, this will fail horribly if you change the example to `t.formatLine("Hello %s!", std::string("monsieur"));` – Praetorian Sep 25 '13 at 16:10
  • Your "working example" is good as long as the resulting string does fit into 256 characters. Also, it is using a Microsoft-specific, non-portable `vsprintf_s` function; this might or might not be a concern for you. – Igor Tandetnik Sep 25 '13 at 16:29
  • Why `buf.size() + strlen(format.c_str())`? This doesn't make any sense. I suggest you pass `_TRUNCATE` as the third parameter of `vsnprintf_s`. And of course, you still have a problem if the resulting string is longer than 256 characters. – Igor Tandetnik Sep 25 '13 at 16:33
  • I'm trying to make the resulting string size dynamic - out of curiosity - 256 should be more than enough for me right now. The non-portable function is not a concern. – phew Sep 25 '13 at 16:36

2 Answers2

11

A Production Quality Answer

#include <cstdarg>
#include <string>
#include <vector>

// requires at least C++11
const std::string vFormat(const std::string sFormat, ...) {

    const char * const zcFormat = sFormat.c_str();

    // initialize use of the variable argument array
    va_list vaArgs;
    va_start(vaArgs, sFormat);

    // reliably acquire the size from a copy of
    // the variable argument array
    // and a functionally reliable call
    // to mock the formatting
    va_list vaCopy;
    va_copy(vaCopy, vaArgs);
    const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaCopy);
    va_end(vaCopy);

    // return a formatted string without
    // risking memory mismanagement
    // and without assuming any compiler
    // or platform specific behavior
    std::vector<char> zc(iLen + 1);
    std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
    va_end(vaArgs);
    return std::string(zc.data(), zc.size()); } 

#include <ctime>
#include <iostream>
#include <iomanip>

// demonstration of use
int main() { 

    std::time_t t = std::time(nullptr);
    int i1 = 11; int i2 = 22; int i3 = 33;
    std::cerr
        << std::put_time(std::localtime(& t), "%D %T")
        << vFormat(" [%s]: %s {i1=%d, i2=%d, i3=%d}",
                "DEBUG",
                "Xyz failed",
                i1, i2, i3)
        << std::endl;
    return 0; }
Douglas Daseeco
  • 3,475
  • 21
  • 27
4

First, it's buffer.c_str() and format.c_str() (note the parentheses).

Second, the first parameter of vsprintf should be a modifiable buffer of sufficient size. You are trying to pass a const char* pointing to a buffer just one byte large.

You could use vector<char> as a buffer holder (it's easy to resize). The problem is, there's no way to get the required buffer size out of vsprintf. One technique is to allocate some initial buffer, then call vsnprintf (note the 'n') repeatedly, doubling the size of the buffer every time the function says it's too small.

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • Could you maybe give me an example on how to use vector in this case? I just can't figure out how to use it with vsnprintf(). – phew Sep 25 '13 at 16:01
  • `vector buf(1024); vsnprintf(&buf[0], buf.size(), format.c_str(), args);`. Resizing the buffer is left as an exercise for the reader. – Igor Tandetnik Sep 25 '13 at 16:06
  • @phew `vsnprintf` is the right idea, but you shouldn't have to call it repeatedly. Make `buffer` a `vector` and call `vsnprint(&buffer[0], 0, format.c_str(), args);`. If the return value is non-negative, call `buffer.resize(retval+1);` and then `vsnprintf(&buffer[0], retval+1, format.c_str(), args);` – Praetorian Sep 25 '13 at 16:08
  • @Praetorian: Interesting. I was going by [MSDN documentation](http://msdn.microsoft.com/en-us/library/1kt27hek.aspx), which states that `vsnprintf` returns the number of characters written if successful, and -1 when buffer is too small. I've now checked the C99 standard, which says that `vsnprintf` returns the required size of the buffer, and thus can be used to measure it in advance. I wonder (but am too lazy to check) whether MSVC implementation is non-conforming, or whether the documentation is wrong. – Igor Tandetnik Sep 25 '13 at 16:13
  • @phew And make sure you give the vector an initial size of at least 1, otherwise `&buffer[0]` will be undefined behavior. – Praetorian Sep 25 '13 at 16:14
  • @Igor Yeah that does seem non-conforming. I don't have MSVC installed on this machine (and probably would be too lazy myself to test anyway :)) to verify whether the implementation or documentation is broken. – Praetorian Sep 25 '13 at 16:19
  • 2
    @Praetorian: Also, per C99, the first parameter may be `NULL` if the second is 0, so one can do `vsnprint(NULL, 0, format.c_str(), args);`. This removes the concern of doing `&buf[0]` on a zero-sized vector. Also, one shouldn't pass the same `va_list` value twice to two `vsnprintf` calls; per 7.15p3, the value becomes indeterminate after the first call. One must use `va_start` again, or else make a copy with `va_copy` beforehand. – Igor Tandetnik Sep 25 '13 at 16:24
  • @Praetorian I'm not sure if that is a proper solution but I just EDITed the post and added a working function - is this the way to go? – phew Sep 25 '13 at 16:25