|
|
|
Sponsored Link •
|
|
Advertisement
|
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]).
|
Sponsored Links
|