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:
So instead of mixing different printing conventions, what if we could just print dave like this:
So lets see how we do that:
Ok, so we cleaned up our print method, now lets show some more stream opperator goodness. Take for example the next class:
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:
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
So lets get right to it. Say you have a Person as such:
Code: C++
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:
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
};
Code: C++
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?
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 );
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:
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.dave is 33 years old.
So lets see how we do that:
Code: C++
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.
// 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;
}
Ok, so we cleaned up our print method, now lets show some more stream opperator goodness. Take for example the next class:
Code: C++
Now if we take the same approach to printing here we can come up with the ultra
clean syntax:
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;
};
Code: C++
Which will print:
Family f; f.addPerson( Person( "dave", 33 ) ); f.addPerson( Person( "sarah", 28 ) ); cout << f << endl;
Output:
And lets take a quick look at how the nesting of these stream insertion opperators
looks:
Family has 2 family members: dave is 33 years old. sarah is 28 years old.
Code: C++
So you see how nesting these things can make for some very clean calling code.
Also it makes debuging complex classes a sinch.
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;
}
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++
Wow! That was easy! Now lets read them back.
// 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();
Code: C++
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.// 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;
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