|
|
|
The C++ Source |
C++ Community News |
Discuss |
Print |
Email |
Screen Friendly Version |
Previous |
Next
|
|
Sponsored Link •
|
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.
ostringstreamcin, 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 typedeffor
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 ostringstreamthis 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 strings. 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,
Bjorn Karlsson and Matthew Wilson
FILE* family of functions.
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.
Bjorn and Matthew keep a reasonably up-to-date listing of their publications at http://bigboyandrunningbear.com/.
|
Sponsored Links
|