std::stringstream
, and extol the virtues of making your classes streamable.
Often, one finds oneself needing to convert arbitrary types to strings. Be it to present a message to a user, to write information to a log file, or to serialize data for subsequent deserialization, tools for string conversions are kept right at the front of the shed—and because they are used so frequently, these tools rarely tarnish. But perhaps one multipurpose power- tool would suffice?
In this article, we will take a look at how to convert arbitrary types to strings using tools from the C++ Standard Library. For ages, C++ programmers have resorted to unsafe conversions from built-in types using printf
. Although we know of exactly two people who never make mistakes with printf
(we can give no names, you understand, but Bjorn is definitely not one of them), the rest of the C++ world could use a safer alternative. One such alternative is a Boost library called Boost.Format, which offers type-safe formatting functionality, including formatting of user-defined types. However, in this installment we shall not wander off the premises of the C++ Standard Library.
ostringstream
cin
,
cout
,
cerr
,
clog
, and their wide-character companions,
wcin
,
wcout
,
wcerr
, and
wclog
. Of specific interest for this installment is
std::ostringstream
, which is actually a
typedef
for
basic_ostringstream<char>
, and is a stream whose underlying storage is in the form of a string (as opposed to an (abstraction of an) input/output device). It is defined in the standard header file called
<sstream>
, which you will need to include in your applications. You use it like any other stream: you stream things to it just as you would with
std::cout
. To retrieve the resulting string (of type
std::string
), you call the member function
str()
. Here's a simple, but complete, example of how to use it:
#include <sstream> #include <iostream> int main() { std::ostringstream stm; // Output an int stm << 31; // Output a char stm << ' '; // Output a double stm << 9.87654; // Retrieve the resulting string std::cout << stm.str() << '\n'; }First, we created an object of type
std::ostringstream
called
stm
. Just like with other output streams, we were able to output data to it - here, an
int
, followed by a
char
, and finally a
float
. Extracting the stored string is easy with the
ostringstream
member function
str()
— in the example we simply printed that string to
std::cout
.
For a number of applications, using ostringstream
this way works like a charm for the conversion problems one encounters. A more practical example than what you just saw would be to mix numeric and text data and store it in a string. Let's assume that you need to format a string that describes the maximum and minimum values for the data type float
:
#include <sstream> #include <limits> #include <iostream> int main() { std::ostringstream stm; stm << "Maximum value for float: " << std::numeric_limits<float>::max() << '\n' << "Minimum value for float: " << std::numeric_limits<float>::min(); std::string values=stm.str(); }In a series of output operations to
stm
, both text and numeric data is written to the string
values
and is formatted the way that the user chooses. (for example,
values
might look like this when running the program:
Maximum value for float: 3.40282e+038 Minimum value for float: 1.17549e-038
#include <sstream> #include <cassert> #include <limits> #include <iomanip> int main() { std::ostringstream stm; // Use a manipulator to select hexadecimal // output of integer values stm << std::hex; // Output an int and a space stm << 31 << ' '; // "1f " // Use a manipulator to select octal output of integer values stm << std::oct; // Output an int and a space stm << 31 << ' '; // "37 " // Use a manipulator to select decimal output of integer values stm << std::dec; // Output an int and a space stm << 31 << ' '; // "31 " // Use a manipulator to select hexadecimal // output of integer values, // and make it uppercase! stm << std::hex << std::uppercase << 31 << ' '; // "1F " // To avoid confusion, show the base! stm << std::showbase << std::hex << 31 << ' ' << // "0X1F" std::oct << 31 << ' ' << // "037" std::dec << 31; // "31" // Retrieve the resulting string assert(stm.str()=="1f 37 31 1F 0X1F 037 31"); }The example demonstrates how the manipulators
std::hex
,
std::oct
, and
std::dec
, are used. The manipulators are provided for syntactical convenience; it's also possible to achieve the effect of these manipulators by explicitly setting the correct
format flags for the stream. For example, the manipulator
std::hex
performs the equivalent of this code that uses the stream member function
setf()
:
#include <sstream> #include <cassert> #include <limits> int main() { std::ostringstream stm; // Default output stm << 31 << ' '; // "31 " // Use a manipulator to select hexadecimal output // of integer values std::ios_base::fmtflags flags= stm.setf(std::ios_base::hex,std::ios_base::basefield); // Output an int stm << 31; // "1f" // Restore the formatting flags stm.setf(flags,std::ios_base::basefield); stm << ' ' << 31; // " 31" assert(stm.str()=="31 1f 31"); }As you can see, manipulators offer a syntactically convenient way of manipulating the format flags of streams. Besides a number of predefined manipulators in the IOStreams library, you can define your own and have them work with any kind of stream. We've already seen
hex
,
oct
,
dec
, and
uppercase
; there are plenty more where those came from [
2,3].
ostringstream
(other output streams too, of course). But what about user-defined types? How can we make our own classes fit snugly in this scheme? It's actually a straightforward task: we must simply provide an appropriate
operator<<
for such types. Doing so is often easier than you might think, especially if your classes mainly contain fundamental types and/or C++ Standard Library types. You must decide which information makes sense to output; typically, this is information that represents the current state of an instance of the type. Consider a class,
Person
, which contains information about a person. It holds a person's
first name,
last name,
age, and
gender. Here's how the definition of
Person
might look:
enum Gender { Female, Male }; class Person { public: Person(const std::string& firstName, const std::string& lastName, int age, Gender gender) : firstName_(firstName), lastName_(lastName), age_(age), gender_(gender) {} std::string FirstName() const { return firstName_; } std::string LastName() const { return lastName_; } int Age() const { return age_; } Gender GetGender() const { return gender_; } private: std::string firstName_; std::string lastName_; int age_; Gender gender_; };With a class like this, all the information required to support output streaming is part of the public interface (through the member functions
FirstName()
,
LastName()
,
Age()
, and
GetGender()
) , but there's still no direct way of streaming an instance of
Person
. To have a
Person
written to
std::cout
, we could use
std::ostringstream
like this:
int main() { Person matt("Matthew","Wilson",36,Male); Person bjorn("Bjorn","Karlsson",31,Male); std::cout << "Name: " << matt.FirstName() << " " << matt.LastName() << ". " << "Age: " << matt.Age() << ". " << "Gender: " << (matt.GetGender()==Female ? "Female" : "Male") << ".\n"; std::cout << "Name: " << bjorn.FirstName() << " " << bjorn.LastName() << ". " << "Age: " << bjorn.Age() << ". " << "Gender: " << (bjorn.GetGender()==Female ? "Female" : "Male") << ".\n"; }Of course, there are a couple of drawbacks with this approach. First, it is tedious to have to "manually" stream all of the data we're interested in to the stream before retrieving the
string
value. Second, such a solution is a little too flexible; undoubtedly, we will see slightly different format of the output for
Person
in different locations of the code—where programmer
A thinks that the representation above makes sense, programmer
B may decide to change the order of the data, or the captions, or something else—this is unprofessional and unnecessarily hard to maintain.
A better approach is to add intrinsic support for output streaming of Person
. We can do this by defining an appropriate operator<<()
, like so:
std::ostream& operator<<(std::ostream& stm, const Person& P) { stm << "Name: " << P.FirstName() << " " << P.LastName() << ". " << "Age: " << P.Age() << ". " << "Gender: " << (P.GetGender()==Female ? "Female" : "Male") << ".\n"; return stm; }The implementation is quite trivial; it simply streams the members of
Person
to the
std::ostream
instance
stm
. By using
std::ostream
as the type of stream, we ensure that
Person
supports streaming to almost any type of
char
-based stream, because
std::ostream
is a
typedef
for
basic_ostream<char,char_traits<char> >
, which is a public base class of all (narrow) standard output streams; to support wide streams we'd need to parameterize the output operator on the character type. (Note: For many classes,
operator<<()
can be defined as a template, and thus independent of any specific stream types; many of the classes in the STLSoft [
3] libraries use this technique to minimize coupling.) Using this augmentation to our
Person
class, here's how the rewritten example above would look:
int main() { Person matt("Matthew","Wilson",36,Male); Person bjorn("Bjorn","Karlsson",32,Male); std::cout << matt; std::cout << bjorn; }Now, that's quite obviously far more succinct, avoids repetitious coding, and works for virtually any type of stream. For example, if we wanted a
std::string
representation, we could make use of the
std::ostringstream
class that we've already learned about:
int main() { Person matt("Matthew","Wilson",36,Male); Person bjorn("Bjorn","Karlsson",32,Male); std::ostringstream stm; stm << matt; std::string MatthewAsString=stm.str(); stm.str(""); // Clear the stream! stm << bjorn; std::string BjornAsString=stm.str(); assert(MatthewAsString == "Name: Matthew Wilson. Age: 36. Gender: Male.\n"); assert(BjornAsString == "Name: Bjorn Karlsson. Age: 32. Gender: Male.\n"); }We could also take things one step further and also make the enumeration
Gender
support output streaming, which leads to an even simpler implementation of
operator<<
for
Person
:
std::ostream& operator<<(std::ostream& stm, Gender g) { stm << (g==Female ? "Female" : "Male"); return stm; } std::ostream& operator<<(std::ostream& stm, const Person& P) { stm << "Name: " << P.FirstName() << " " << P.LastName() << ". " << "Age: " << P.Age() << ". " << "Gender: " << P.GetGender() << ".\n"; return stm; }As you can see, one can quickly reap the benefits of OutputStreamable[ 4] types. Rather than leaving the chore of performing stream output by hand to clients of your classes, make sure that they play nicely with IOStreams. We intentionally omit the discussion on an important topic here; localization. Different types may have different output depending on the locale (a component that encapsulates cultural-specific information) that is imbued on the stream—simply put, the locale that the stream uses for formatting of culture-specific output. For example, dates have different formats in different countries, and thus, different locales have different ways of outputting dates. Localization is an important, but also complex, topic. We shall talk about localization in a future installment, but for now, you and your classes are free to ignore them (or read up on the subject in [ 5]).
std::ostringstream
, but it's not the only string stream in
IOStreams; it is complemented by
std::istringstream
for input, and
std::stringstream
for both input and output. The following example demonstrates a simple use of
istringstream
to extract data from a string:
#include <sstream> #include <string> #include <cassert> int main() { std::istringstream stm; stm.str("1 3.14159265 Two strings"); int i; double d; std::string s1,s2; stm >> i >> d >> s1 >> s2; assert(i==1); assert(d==3.14159265); assert(s1=="Two"); assert(s2=="strings"); }As you can see in the example, the
istringstream
is assigned a string that is subsequently used to extract data from; an
int
, a
double
, and two
string
s. By default,
istringstream
skips whitespace; if you need to change that behavior you can use the manipulator
std::noskipws
.
The remaining class, std::stringstream
, does both input and output streaming. When using these streams, it makes sense to always use the stream with the capabilities you are looking for. This makes it easier to read and understand the code, so if all you need is input (string) streaming, use std::istringstream
rather than always going with std::stringstream
for convenience. (Remember, what may be convenient at the time of writing may not be convenient when the same code needs to be maintained.)
#include <sstream> #include <string> #include <cassert> #include <iostream> int main() { std::istringstream stm; stm >> std::noskipws; // Don't skip whitespace stm.str(" 1.23"); double d; stm >> d; if (!stm) { std::cout << "Error streaming to d!\n"; // Manually fix the problem...assuming we know what went wrong! // In this example, we know that we must ignore whitespace, // so we simply clear the stream's state. stm.clear(std::ios::goodbit); // Ignore whitespace! stm >> std::skipws; stm >> d; } assert(d==1.23); }In the example, the extraction of a double will fail on the first attempt; because there's no way a space can be converted to a double. This leaves the stream in a bad state (
std::ios::failbit
will be set), which is why the test
if (!stm)
yields
true
. Once a stream has gone bad, you must explicitly set it in a good state to be able to use it again. In this example, we know what's gone wrong, and we decide to turn on the skipping of spaces again, which nicely resolves the problem. Then, and only then, can we successfully extract the
double
!
Checking the stream's state can be tedious and is easy to forget. An alternative is to tell the stream to use exceptions when entering a bad state. This is done through a member function called exceptions
, which accepts an argument that denotes which bad states should cause an exception to be thrown. The recommended mask includes the flags badbit
and failbit
. Here's the above example in a version using exceptions:
#include <sstream> #include <string> #include <cassert> #include <iostream> int main() { std::istringstream stm; stm >> std::noskipws; // Don't skip whitespace stm.str(" 1.23"); double d; try { // Turn on exceptions stm.exceptions(std::ios::badbit | std::ios::failbit); stm >> d; } catch(std::ios_base::failure e) { std::cout << e.what() << '\n'; // Manually fix it...assuming we know what went wrong! stm.clear(std::ios::goodbit); // Ignore whitespace! stm >> std::skipws; stm >> d; } assert(d==1.23); }Whether to use exceptions or not when streams end up in a bad state largely depends on the problem at hand. Our advice is to consider how a stream in a bad state affects your code, and if failure indicates a truly exceptional situation, then the exception-throwing version is definitely better.
std::ostringstream
,
std::istringstream
and
std::stringstream
. In most cases (and when you're not in need of blistering performance [
6,7]), rather than searching for a suitable (specialized) conversion function, this stream type can be used to convert most anything to its string representation. This led us to another important topic, namely enabling your own classes to work seamlessly with output streams by making them
OutputStreamable. Finally, we looked at other string stream offerings from the C++ Standard Library. Conversions from various types to strings are ubiquitous in most any application, which means that all the measurements we take to simplify such conversions bring forth excellent value. Input streaming, which was only briefly discussed here, is just as important as output streaming—that will be the topic of a future article.
We hope that the tools covered in this article helps you feel empowered to do what the title says—Stream Thy Strings!
Thank you for reading,
FILE*
family of functions.Have an opinion? Readers have already posted 2 comments about this article. Why not add yours?
Bjorn Karlsson is proud to be a C++ designer, programmer, teacher, preacher, and student. He has finally learned enough about C++ to realize how little he knows. When not reading or writing articles, books, or code, he has the privilege to be a part of the Boost community, and a member of The C++ Source Advisory Board. His book, Beyond The C++ Standard Library: An Introduction to Boost, will be published by Addison-Wesley in 2005. He appreciates it when people send him interesting emails at bjorn.karlsson@readsoft.com.
Matthew Wilson is a software development consultant, contributing editor for C/C++ User's Journal, and creator of the STLSoft libraries (http://stlsoft.org/) . He is author of Imperfect C++ (Addison-Wesley, 2004), and is currently working on his next two books, one of which is not about C++. Matthew's appetite for coding challenges is matched only by his appetite for chocolate; he keeps them in check by writing articles and riding his bike (not always at the same time). He can be contacted via http://imperfectcplusplus.com/.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.