Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've never been satisfied with any of the possible sprintf()-like functions in C++:

  • sprintf() is a C function and it's very unsafe and dangerous to use (undefined behavior all over the place)
  • boost::format is too slow and awkward to use (think of the % syntax)
  • iostream is extremely awkward to use

So I came up with my own. Those are my requirements:

  • Inspired by C#'s format function
  • Must never fail at run time, never crash and never have undefined behavior, no matter what you call it with. Should fail at compile time whenever possible instead.
  • Must be expandable (if you create a class Person, then you can pass a Person object to the function)
  • Must be fast
  • The format string must be easy to localize
  • The format string must not have placeholders whose syntax depends on the argument type (ie, no %s vs %d)
  • Must have a fairly "normal" syntax (no % like boost::format)

I'm interested in reviews on the satisfaction of the requirements above. I'm especially interested in reviews from the end-user perspective and less about the internals of the code (i.e., the code that is hidden from the end user).

A few examples:

// Returns a string containing "Hello my name is Andreas and I'm 22 years old".
Format("Hello my name is {0} and I'm {1} years old.", "Andreas", 22);

// Returns "Hex: 0xA"
Format("Hex: {0:x}", 10);

// Fails to compile: Person is not a built in type and doesn't have any function to convert to string
struct Person {};
Person p;
Format("Person: {0}", p);

// "Person: Andreas Bonini" [Note: if it was {0:abc}, then Person::ToString("abc") would have been called]
struct Person {
   Person(string first, string last) : First(first), Last(last) {}
   string ToString(const string &options) const { return Format("{0} {1}", First, Last); }
   string First, Last;
};
Person p("Andreas", "Bonini");
Format("Person: {0}", p);

These are my unit tests:

TEST(Format)
{
    CHECK_EQUAL(Format("Testing {0} test {0}{0} test", 123), "Testing 123 test 123123 test");
    CHECK_EQUAL(Format("{0}", (const char *)NULL), "{null}");

    CHECK_EQUAL(Format("Test double {0} - {1}", 0, 1), "Test double 0 - 1");
    CHECK_EQUAL(Format("Test options: {0:x}", 0xABC), "Test options: 0xABC");
    CHECK_EQUAL(Format("Test malformed: {0:x", 0xABC), "Test malformed: {0:x");
    CHECK_EQUAL(Format("Check stupid: {0:bmkldmbkglmbgk902 r iko4om wkl lfs s,gf, gfsdg fsd ! @ G}", "stupid"), "Check stupid: stupid");
    CHECK_EQUAL(Format("Check missing: {1}", 0), "Check missing: {Argument 1}");

    CHECK_EQUAL(Format("Master format test {0} {1} {2 {2:2} {3} {4:e} {5:x} {0}", 0, 1234.55566f, 1.11111f, "lolz", "a'b", 0xABCDEFABCDEFULL),
        "Master format test 0 1234.56 {2 1.11 lolz 'a\\'b' 0xABCDEFABCDEF 0");

    CHECK_EQUAL(Format("{0:x}", 0xFFFFFFFF), "0xFFFFFFFF");
}

Note that due to C++ limitations (lack of variadic templates) the code isn't exactly pretty. Also the code is part of a bigger library so it won't compile of its own.

share|improve this question
    
PS: comments in the code talk about alignment, the {0,10} syntax, which is not implemented yet. Also I wrote my examples (but not the unit tests) without trying to run them first. –  Andreas Bonini Apr 30 '11 at 14:26
    
You might be interested in the C++ Format library cppformat.github.io. It is based on Python's str.format which uses very similar format string syntax. It is also very fast, depending on format string it can even be faster than sprintf. –  vitaut May 20 '14 at 0:30

3 Answers 3

In my opinion:

  • Use fullscope names (like std::vector, boost::shared_ptr)
  • Don't use const & for simple types (for example int or double)
  • Write const after the type: std::string const&
  • For string IntToString(...) and ToString(const double &val...) why you don't use std::ostringstream?
  • In FormatNoVariadicTemplates.h why you don't use functions for FNVT_BEGIN and FNVT_END?

Have you any some benchmark results?

PS: good idea ;) I'm usually use to Qt functions, we are using this library in our projects or stl.

share|improve this answer
    
For point 1), I prefer to use names without the namespace, it really bothers me having to type it every time 2) I sometimes use it mostly for consistency; can't hurt 3) That's a personal preference 4) std::ostringstreams is extremely slow, and it's the main reason boost::format is over 10 times slower than printf(), even with optimizations on 5) It cannot be converted to a function because of the ## preprocessor trick (see ARGUMENT()) –  Andreas Bonini May 1 '11 at 15:57
    
Few links about some points, it's not very important, but i use it. 1) gotw.ca/publications/c++cs.htm (59) 3) codeproject.com/KB/cpp/… And 5) It can be template functions, i think. –  siquell May 1 '11 at 16:45
1  
The thing about using in headers is important. –  Lstor May 2 '11 at 12:24

There are some serious portability-issues with the code:

  • You do not have proper header guards. #pragma is non-standard.
  • You do not have proper includes (because of a precompiled header, I presume).
  • Names beginning with an underscore followed by an uppercase letter (e.g. _FinalizeFormat, _FindPlaceholder) are reserved by the compiler.
share|improve this answer
1  
For first point i use to #ifndef LIKE_THIS #define LIKE_THIS ... #endif // LIKE_THIS –  siquell May 2 '11 at 12:57
    
@siquell Ditto :) –  Lstor Dec 29 '11 at 20:01

I have a few comments, in no particular order.

It seems to me like you can't decide between std::string and const char*. Pick one.

(Maybe I'll get flamed for this one.) You say you care about performance, but your use of vector and string tell a different story. It seems to me like heap allocation is completely unneccessary for most of this, and for this reason I would avoid STL and C++ strings. Or, if not avoiding them entirely, I would avoid using so many std::strings as temporaries while formatting. The functions that do this should maybe take a destination buffer as a parameter.

It seems strange that your internal representation of options remains a string at all times. For example:

static const char *normalStrings[] = { "true", "false" };
static const char *alternativeStrings[] = { "yes", "no" };
const bool alternative = (options == "a");
const char **strings = alternative ? alternativeStrings : normalStrings;

This itself is a bit hard for an outsider to follow as written, but aside from that, do you really wantoptions == "a"? What if you want more than one option? Wouldn't a check for a be more like strchr(options, 'a')? In which case I would say do you really want O(n) lookup for options? Maybe this doesn't make sense as a string. Maybe you want to store them internally as flags in an integer. Even if you don't want to combine options it still might look less weird as an enum or constant. There are a lot of magic literals floating around in this code.

There's lots of code that's more verbose than it has to be, for example:

/* Find the '{' */
for (int i = start; i < end; ++i)
{
    if (str[i] != '{')
        continue;

    /* Check if there is enough space for the minimum spaceholder length, 3: {.} */
    if (end - i < 3)
        continue;

The for loop seems to be reinventing strchr. I would much prefer something like this:

char *current, *end;

while ((current=strchr(str, '{')) &&
       (end=strchr(current, '}')))
{
   //
   // TODO: do something with placeholder at (current, end)
   //

   str = end + 1;
}

Or perhaps more C-like (though I realize you're writing C++ it looks like you've already rejected some of its idioms) would be something like this:

while (*str)
{
   if (*str == '{' &&
       (end=strchr(str, '}')))
   {
      //
      // TODO: evaluate placeholder at (str, end) into dst
      //

      str = end+1;
   }
   else
   {
      // TODO: make sure this doesn't overflow :-)
      *dst++ = *str++;
   }
}

IntToString could have a lot of improvements... One trivial stylistic thing is that "0123456789" is a lot easier on the eyes than { '0', '1', '2', ... }. More importantly, do you have to do this with a separate heap allocation? Maybe it should be passed a destination buffer. The reserve method being called with a constant also looks a bit weird because you've just done the conversion inside tmp, so you know the exact length...

Lastly, as a matter of habit, if you're going to use sprintf -- well don't. Use snprintf instead. (If you happen to be using a version of Visual Studio that doesn't have that, there's also sprintf_s or StringCchPrintf.)

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.