5

Very frequently in my C++ code I use the following type of helper function:

static inline std::string stringf(const char *fmt, ...)
{
    std::string ret;
    // Deal with varargs
    va_list args;
    va_start(args, fmt);
    // Resize our string based on the arguments
    ret.resize(vsnprintf(0, 0, fmt, args));
    // End the varargs and restart because vsnprintf mucked up our args
    va_end(args);
    va_start(args, fmt);
    // Fill the string
    if(!ret.empty())
    {
        vsnprintf(&ret.front(), ret.size() + 1, fmt, args);
    }
    // End of variadic section
    va_end(args);
    // Return the string
    return ret;
}

It has a few upsides:

  1. No arbitrary limits on string lengths
  2. The string is generated in-place and doesn't get copied around (if RVO works as it should)
  3. No surprises from the outside

Now, I have a few of problems with it:

  1. Kind of ugly with the rescanning of the varargs
  2. The fact that std::string is internally a contiguous string with space for a null terminator directly after it, does not seem to actually be explicitly stated in the spec. It's implied via the fact that ->c_str() has to be O(1) and return a null-terminated string, and I believe &(data()[0]) is supposed to equal &(*begin())
  3. vsnprintf() is called twice, potentially doing expensive throw-away work the first time

Does anybody know a better way?

Yuriy Romanenko
  • 1,454
  • 10
  • 19

3 Answers3

5

Are you married (pun intended) to std::sprintf()? If you are using C++ and the oh so modern std::string, why not take full advantage of new language features and use variadic templates to do a type safe sprintf that returns a std::string?

Check out this very nice looking implementation: https://github.com/c42f/tinyformat. I think this solves all your issues.

MSalters
  • 173,980
  • 10
  • 155
  • 350
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
5

As you added the tag c++11 I suppose that you can use it. Then you can simplify your code to this:

namespace fmt {

template< class ...Args >
std::string sprintf( const char * f, Args && ...args ) {
    int size = snprintf( nullptr, 0, f, args... );
    std::string res;
    res.resize( size );
    snprintf( & res[ 0 ], size + 1, f, args... );
    return res;
}

}

int main() {
    cout << fmt::sprintf( "%s %d %.1f\n", "Hello", 42, 33.22 );
    return 0;
}

http://ideone.com/kSnXKj

Arto Bendiken
  • 2,567
  • 1
  • 24
  • 28
borisbn
  • 4,988
  • 25
  • 42
1

Don't do the first vsnprintf with size 0. Instead, use a stack buffer with some probable size (e.g. something between 64 to 4096), and copy that into the return value if it fits.

Your concerns about std::string's contiguity are misplaced. It is well-defined and perfectly okay to rely on.

Finally - I would like to reecho that a compile-time format-checking library would be better. You need C++14 for full power, but C++11 supports enough that you should be able to #ifdef some header code without losing any expressive. Remember to think about gettext though!

o11c
  • 15,265
  • 4
  • 50
  • 75
  • Having some probable size buried in shared utility code is a recipe for disaster. Having it on the stack is also probably not a good idea -- because you're guaranteed at least two additional problems on the day you are debugging why somebody is "%s" printing a 50MB string. – Yuriy Romanenko Jun 26 '15 at 05:25
  • @YuriyRomanenko What? It's no disaster, and it's fixed size. *Any* size, including 0, is valid (excepting that 0-size arrays are an extension) for the first round. It's easy enough to unit-test one string smaller and one larget than the size. Reasonable values range from 64 to 4096. – o11c Jun 26 '15 at 05:36
  • I don't understand. I can sprintf as much as I can fit in memory, I can make a std::string as long as my memory, why would I limit my function to 4096 bytes? – Yuriy Romanenko Jun 26 '15 at 05:40
  • 1
    @YuriyRomanenko: You keep the logic for dynamically allocating and printing again, which allows nearly unlimited output length, except that you skip that if it fit the first time. It's an optimization, not a limitation. – Ben Voigt Jun 26 '15 at 05:45
  • Oh I see, that does sound like an improvement in terms of performance -- assuming that sprintf to NULL is almost always slower than a memcpy – Yuriy Romanenko Jun 26 '15 at 05:47
  • @YuriyRomanenko: `sprintf` is slower by at least an order of magnitude, if only because it needs to copy its input literally to its output (best case, format string just `"%s"`). – MSalters Jun 26 '15 at 08:36