Developer

C++: In search of the perfect convert-to-string function

You can take a variety of approaches when converting an object into a stream in C++. Follow along as I use STL to develop an efficient, generic conversion function that will handle any data type with an overloaded operator.


Software developers and programmers often want to convert objects to strings. For example, imagine printing a message box with the number of words in a document. There are several ways to do this, as Listing A shows: Use sprintf, use ecvt, or use the nonportable _itoa function (found in Win32).

All of these techniques come from the C programming language, and each has drawbacks:
  • The sprintf is not type-safe, needs a buffer, and is pretty hard to use.
  • The syntax of ecvt is peculiar, complex to grasp, and it works only for numbers.
  • The _itoa works only for integers, and, even worse, it's not portable.

In this article, I'll show you an easy and efficient way to convert any type to a string. It will work for any type that has an overloaded operator <<. As a bonus, it's a generic solution—it will work for any string type: std::string, std::wstring (wide string), etc.

Version 1.0: to_string
First of all, to be type-safe, it will have to work for any types. Thus, we use templates:
template< class type> std::string to_string( const type & value)
{ /* code */ }


This solution comes from Standard Template Library (STL) streams. Besides file streams (basic_[i/o]fstream classes), STL comes with string streams. A string stream has an underlying character sequence. After you have written your data, you can treat it as a string and query the str() member function, which will return a std::(basic_)string. Listing B shows the first version of to_string; its simplicity is quite astonishing.

The to_string works for any type that has an overloaded operator <<. This includes the built-in types, like I and char.

Why use to_string?
The behavior of to_string can be emulated, as shown in Listing C. It's a matter of taste whether you'll prefer to_string. One reason to favor it is debugging. When you watch a std::string variable, you'll definitely see a const char * pointer (a string, as you expect). This is not necessary true for a stringstream because its underlying implementation might contain multiple buffers that are concatenated only when the str() function is called.

Version 1.5: str_stream
The to_string template pointed us in the right direction, but you may also want to do something like what's shown in Listing D. However, this method is not portable and can be dangerous.

We can get close to Listing D by having a function that returns a proxy with an underlying stringstream. Everything that gets written to the proxy is written to the underlying stringstream, as Listing E shows. Then, it's implicitly converted to a string (operator std::string()).

If you want explicit conversion, you can use as_string, which now has no side effects (Listing F).

Implementing as_string complicates the code, however, due to a bug in Visual C++ 6. Here's what the code does:
  • If you have a value str_stream() << some_value, you return const str_stream & (the way streams behave).
  • If you have as_string (str_stream << … << as_string()), you return a string.

Since you will write less code, you will most likely prefer str_stream() to to_string().

Efficiency
Most important, code using str_stream() will be more efficient compared to to_string because a string stream is much more optimized for appending than string. Don't take my word for it: Run the test shown in Listing G. Depending on your STL implementation, you'll see that str_stream() can run at least two times faster than to_string(). The more complicated the string expression is, the faster str_stream() will be in comparison.

Debugging
As I stated above, one reason to prefer to_string() is its debugging capabilities. We can also make str_stream() debug-friendly: In debug mode, we'll keep a string in addition to the stream (Listing H).

Keeping a string instead of a stream is not a good idea; check out the comments in Listing I to see why.

Version 1.6: Including formatting
To be able to format, you have to allow IO manipulators to be written to your str_stream(). There are two types of IO manipulators:
  • Functions (std::oct, std::dec, std::scientific, etc.)
  • Objects (std::setw, std::setfill, etc.)

Our implementation allows for IO manipulators that are objects because we allow any object to be written to our stream. Let's allow for functions as well (Listing J).

Version 1.9: to_string like usage
Sometimes, you'll still prefer to_string to str_string. For example, when you just want to add one value:
// using to_string
s += to_string( nUsersCount);
// using str_string
s += str_string() << nUsersCount;


Let's make str_string behave like to_string. All we need to do is add a templated constructor, which calls << for its argument. To compile it, I had to rearrange the code a bit (>Listing K).

Version 6.0: Make it possible to work with any type of char
So far, str_stream works only with char streams. A generic solution should work for any stream. Even though this will complicate the code, it's well worth it. The new str_stream models existing STL code: There is a basic_str_stream with two templated parameters: char_type and char_traits.

There are two typedefs: str_stream and wstr_stream. Existing client code will still work. In >Listing L, you'll notice that there are four files: str_stream.h, str_stream_vc_before.h, str_stream_vc_after.h, and example.cpp. The str_stream_vc_[before/after].h files contain workarounds for Visual C, while example.cpp shows an example of using str_stream.

Possible improvements
Our str_stream function should work with any type that has an overloaded operator <<, but this will print some bogus data:
const wchar_t * strWide = L"wide string";
std::string s = str_stream() << strWide;


This is because you're trying to write a wide string to a narrow stream. The wide string is treated as a pointer, and, therefore, the bogus data will be stored into s. However, the same thing will happen for any stream as well:
// will output bogus data (a pointer' value)
std::cout << L"wide string";


Of course, you'd like an automatic conversion to take place when writing to str_stream(), like this:
const wchar_t * strWide = L"I got home from work. ";
// store "I got home from work." into s
std::string s = str_stream() << strWide;


This is a valid request, and it's not so hard to implement. However, it's hard to implement in an efficient manner. Here's why:
  • When writing a value, we must determine whether it's a string.
  • If it's not a string, write it as before.
  • If it's a string, determine whether it can be written as is.
    —If it can be written as is, we'll write it without creating any temporaries
    —If it can't, we'll convert and then write it.

As hard as automatic conversion might be, it's definitely worth it.

How'd I do?
Implementing a generic str_stream() is not an easy task, but I really enjoyed it (except for the VC bugs). I developed it because of the need to write any data to a string. I did it with these goals in mind: to be efficient, type-safe, and simple to use while remaining generic. Did I succeed? Please let me know—post a comment in the article discussion.

Editor's Picks