This article introduces a C++ class that facilitates the composition of arguments from varying types into a string. Currently, the C++ standard library does not have a counterpart to the .NET
System.Text.StringBuilder. Still, the need to embed the values of parameters of varying types inside a string is very common, e.g. when logging information to a file or when presenting the user with a message.
StringBuilder class presented in this article addresses this need. It relies on features inherent to C++ and provides the user with a neat and compact way of creating strings on the basis of parameters of varying types.
Lately, while implementing a thread-safe cyclic buffer, I came across the need for a convenient mechanism for logging the state of the buffer to a file. This meant being able to create a string composed of the values of parameters of different types (which together govern the state of the buffer) and pass it to a logging function, which would then write it to a file. One way of achieving this functionality is through
int index = 0;
sprintf(string_buf, "%Current index is: %d.", index);
The downside of this approach is that it is very cumbersome. Our need to create a formatted string that contains the value of a single parameter is translated into four lines of C++ code. Moreover, we need to allocate a buffer to be used by
sprintf and the decision of how large it should be cannot (in most cases) be made in advance. We are thus exposed to the risk of buffer overflow.
What comes to mind at this point is the idea of wrapping around the
fprintf's (somewhat simplified) signature is as follows:
int fprintf(FILE *_file, const char *_format, ...);
The basic idea behind
fprintf is that is uses its second argument (
_format), which is of type
const char *, to read the arguments that come after. The
... argument makes
fprintf a variadic function (i.e. a function that accepts a variable number of arguments). It thus seems that if we expose a logging function with the following signature...
int log(const char *format, ...);
...then we can pass its received arguments to
fprintf and thus achieve our goal with the following code:
int log(const char *format, ...)
FILE *pLogFile = fopen("log.txt","w");
int retVal = fprintf(pLogFile, format, argumentsList);
Unfortunately, although this code may compile, it is unlikely to do what we opt for. This is because our call to
printf is seen -- from the compiler's point of view -- as a procedure call involving only two arguments, which clearly violates our intent to support an arbitrary number of arguments.
Back to the drawing board... It now seems that we have two options available:
- Implement the mechanism used by the
printf family (which consists of a format string and an arbitrarily long list of arguments that comes after) all over again.
- Do something way more cool, which relies on conversion operators, operator overloading and implicit constructors. This innovative approach should also rely on the fact that C++ is statically typed to make sure that problems with the resultant string will be caught by the compiler, rather than at runtime (this guarantee cannot be made by the
Naturally, I went for the second option.
Using the Code
As you may have guessed already, the code I came up with uses all the treasures I named above. Fundamentally, it's divided into two classes,
StringElement objects can be constructed using a variety of types. None of
StringElement's constructors is explicit, which implies that it can be implicitly constructed from a long (and extensible) list of types.
operator<< makes use of this fact:
StringBuilder &operator<<(StringElement se)
Append(se); return *this;
It accepts an argument of type
StringElement and appends it to its
_value member -- which is of type
std::string -- using the following code:
void Append(StringElement element)
For this code to compile,
StringElement must be convertible to
std::string, and indeed it is. Since the return value of
StringBuiler & (i.e. a
StringBuilder reference) and since
operator<< is left-associative, it supports cascading calls. Last but not least,
StringBuilder is convertible to both
char * and
std::string, which allows for the following code to compile:
printf(StringBuilder() << "x=" << x << " and y=" << y);
The only bit that's left is to hide from the user the fact that a
StringBuilder object is actually created as part of the call to
printf. This is done using a simple macro. The final result is as follows:
printf(SB << "x=" << x << " and y=" << y);
As I mentioned above, a major advantage of the approach taken here compared to the mechanism used by
printf is that it allows us to catch ill-formatted strings at compile time. This is because the arguments we put in are compiled into
StringElement objects (using the implicit constructors defined on
StringElement) and from there on things should proceed smoothly, as the implementation of
StringElement should make clear. Considering the number of crashes I've experienced due to typos in my format string, I view this as a significant advantage of the approach I have taken.
- 14 September, 2007 -- Original version posted