Navigation
The Beast
IP: 24.34.46.73
Status: checking...
Article: Overloading Stream Operators
All serious C++ coders should know this technique.

So lets get right to it. Say you have a Person as such:
Code: C++
class Person {
private:
	std::string m_name;
	int m_age;
public:
	Person();
	Person( std::string name, int age );
	Person( const Person& p ); // Copy constructor
	
	std::string name() const; // Getter
	void name( std::string name ); // Setter
	
	int age() const; // Getter
	void age( int age ); // Setter
};
						
And now say you wanted to print out information on that person to the std::out. How would you go about doing this? Well you could whip up something like:
Code: C++
				
void printPerson( const Person& p ) {
	cout << p.name() << " is " << p.age() << " years old." << endl;
}

// And call it like so:
Person dave( "dave", 33 );
printPerson( dave );
						
But it now becomes cumbersome to format around it. It won't be explicit mixing that function call along with other cout stream insertions. Having simple explicit syntax is what makes code maintainable, and we do want maintainable code rriigghhtt?

So instead of mixing different printing conventions, what if we could just print dave like this:
Code: C++
Person dave( "dave", 33 );
cout << dave << endl;
						
Output:
dave is 33 years old.
						
We could add in any information around dave that we want and it is all using a uniform convention to do it. There is no question what we are looking at.

So lets see how we do that:
Code: C++
// Here I overload the outstream insertion operator, this alows me to just
// pass a Person object to cout << and have it print with out a problem,
// this is GREAT for fast debuging

ostream& operator <<( ostream& os, const Person& person ) {
    os << person.name() << " is " << person.age() << " years old.";
    return os;
}
						
It's good practice not to have an endl in there for two reasons. First is, what if we don't want to have a new line? That should be left up to the calling code. And second is, endl flushes the buffer as well as appends a new line. This can be slow if you are doing it a lot and thus should be left to the descresion of the calling code.

Ok, so we cleaned up our print method, now lets show some more stream opperator goodness. Take for example the next class:
Code: C++
class Family {
private:
    std::vector<Person> m_family;
public:
    Family();
	
    void addPerson( const Person& person );
    int numFamilyMembers() const;
	
    Person& getPerson( int i );
    const Person& getPerson( int i ) const;
};
						
Now if we take the same approach to printing here we can come up with the ultra clean syntax:
Code: C++
Family f;
f.addPerson( Person( "dave", 33 ) );
f.addPerson( Person( "sarah", 28 ) );
cout << f << endl;
						
Which will print:
Output:
Family has 2 family members:
dave is 33 years old.
sarah is 28 years old.
						
And lets take a quick look at how the nesting of these stream insertion opperators looks:
Code: C++
ostream& operator <<( ostream& os, const Family& family ) {
    int n = family.numFamilyMembers();

    os << "Family has " << n << " family members: " << endl;

    for( int i=0; i < n; ++i ) {
	// This puts the individual people into the stream
        os << family.getPerson( i ) << endl;
    }
    os << endl;

    return os;
}
						
So you see how nesting these things can make for some very clean calling code. Also it makes debuging complex classes a sinch.

Now you must be saying, why yes Adam that is all fairly awesome-tastic, but as my old Data Structures teacher would say, Where is the beef?.

The syntax we simplified for printing was fairly simple to begin with. File I/O how ever can be cumbersome even for the seasoned programmer. Saving the state of something to a file, while not a programming feat to be praised for, can still be very anoying.

But not with our new knowledge of stream operators! Take a look at what we can simplify the file I/O syntax to with the methods we have been discussing:
Code: C++
// First, writing our family 'f' to a file:
    // Open our file stream for writing
    ofstream fout( "family.dat" );
	
    // Serialize our family to the stream
    fout << f;
	
    // Close the file
    fout.close();
						
Wow! That was easy! Now lets read them back.
Code: C++
// Open our file stream for reading
ifstream fin( "family.dat" );

// Create our family to store the deserialized data into
Family f2;

// Get our family from the file already!
fin >> f2;

// Close our file
fin.close();

// Now simply print our family using the
// magic of our overloaded opperators
cout << "Family deserialized from file:" << endl << f2 << endl;
						
I've posted the full source of a test program that will do all of this at the end of the article here so you can check it all out, so I will spare you the deails of the opperator overloads to do that as it is the same concept I have just got done describing in the rest of the article.

Do note though, that I needed to overload both the insertion and extraction opperators to accomplish this, so there are four new functions in the file I/O example. One insertion and one extraction opperator for each class.

As a last thought on all of this, it is not neccisary to friend these operators into your classes and break encapsulation. Infact, it's even more adventagous not to do so. Declaring them out side your classes allows them to be used in situations with implicit construction. That goes for most opperator overloads I can think of.

Well thats all for now, feel free to drop me a line with any thoughts you have about this article.

Happy hacking!

Full Source: family.cpp