The Artima Developer Community
Sponsored Link

The C++ Source
Stream Thy Strings
by Bjorn Karlsson and Matthew Wilson
May 28, 2005

<<  Page 2 of 3  >>

Advertisement

Make Your Classes OutputStreamable

We've seen how well the fundamental types support streaming to an 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]).

<<  Page 2 of 3  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us