C++ variable arguments with std::string only

16,121

Solution 1

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.

Solution 2

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; }
Share:
16,121

Related videos on Youtube

phew
Author by

phew

Just gimme that damn badge already ;-)

Updated on September 15, 2022

Comments

  • phew
    phew over 1 year

    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;
    }
    
    • Xeo
      Xeo over 10 years
      Only trivial types are allowed to be passed to ..., std::string is no such type.
  • phew
    phew over 10 years
    Could you maybe give me an example on how to use vector<char> in this case? I just can't figure out how to use it with vsnprintf().
  • Igor Tandetnik
    Igor Tandetnik over 10 years
    vector<char> buf(1024); vsnprintf(&buf[0], buf.size(), format.c_str(), args);. Resizing the buffer is left as an exercise for the reader.
  • Praetorian
    Praetorian over 10 years
    @phew vsnprintf is the right idea, but you shouldn't have to call it repeatedly. Make buffer a vector<char> 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);
  • Igor Tandetnik
    Igor Tandetnik over 10 years
    @Praetorian: Interesting. I was going by MSDN documentation, 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.
  • Praetorian
    Praetorian over 10 years
    @phew And make sure you give the vector an initial size of at least 1, otherwise &buffer[0] will be undefined behavior.
  • Praetorian
    Praetorian over 10 years
    @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.
  • Igor Tandetnik
    Igor Tandetnik over 10 years
    @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.
  • phew
    phew over 10 years
    @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?