Overriding the Insertion Operator for C++ Template Classes

2011/03/06

Dump an Active Record object to Standard Out:

Person person(123);
std::cout << person << endl;

While working on my C++ ActiveRecord implementation, I had a few problems implementing the insertion operator for the main ActiveRecord::Base class.

The class ActiveRecord::Base class is a template class, and the problem was how to correctly declare the operator.

At the time I was unable to find examples on the Internet, so I thought I'd provide my own.

The Insertion Operator

By "insertion operator" I mean

ostream & operator<<(ostream &out_stream, const C& c);

This is a global function that can be called like this:

std::cout << c;

This operator allows you to output a string representation of your classes, which is very handy for debugging:

C c;
c.do_stuff();
std::cout << c << endl;

The Example

A Class

Here's an example class which (rather reduntantly) wraps an STL std::list:

#include 

using namespace std;

template < class T >
class MyList {
 public:
  MyList() {};

  void add( T item ) {
    data_.push_back( item );
  }

  int length() const {
    return data_.size();
  }

 private:
  list< T > data_;
};

Serialization

While debugging and testing, it would be handy to be able to output a string representation of an instance of this class.

MyList ints;
ints.add(42);
ints.add(13);
std::cout << ints << endl;

Which should work like this:

$ ./my_list
2 items:
  42
  13

Implementation

Here's an implementation that just outputs the number of items:

template< class T >
ostream & operator<<( ostream &out_stream, const MyList< T >& a_list ) {
  out_stream << a_list.length() << " items:" << endl;
  return out_stream;
}

Next, we want the operator to iterate over the list<T> member and output its values.

The main sticking point for me here was the declaration of the iterator. The following is not sufficient:

...
for( list::const_iterator it = a_list.data_.begin(); it != a_list.data_.end(); ++it ) {
...

and produces this error message (with gcc):

to_ostream.cpp: In function 'std::ostream& operator<<(std::ostream&, const MyList&)':
to_ostream.cpp:7: error: expected `;' before 'it'
to_ostream.cpp:7: error: 'it' was not declared in this scope

What's missing is the typename keyword to tell the compiler that we're instantiating an iterator:

...
for( typename list::const_iterator it = a_list.data_.begin(); it != a_list.data_.end(); ++it ) {
...

Here's the final implementation:

template< class T >
ostream & operator<<( ostream &out_stream, const MyList< T >& a_list ) {
  out_stream << a_list.length() << " items:" << endl;
  for( typename list::const_iterator it = a_list.data_.begin(); it != a_list.data_.end(); ++it ) {
    out_stream << "\t" << *it << endl;
  }
  return out_stream;
}

The function also has to access the private `data_` member, which entails declaring it as a `friend`:

template < class T >
class MyList {
 template< class T1 >
 friend ostream & operator<<( ostream &out_stream, const MyList< T1 >& a_list );
 ...
};

Here's the whole class with the operator:

#include 
#include 

using namespace std;

template < class T >
class MyList {
 template< class T1 >
 friend ostream & operator<<( ostream &out_stream, const MyList< T1 >& a_list );
 public:
  void add( T item ) {
    data_.push_back( item );
  }
  
  int length() const {
    return data_.size();
  }
 private:
  list< T > data_;
};

template< class T >
ostream & operator<<( ostream &out_stream, const MyList< T >& a_list ) {
  out_stream << a_list.length() << " items:" << endl;
  for( typename list::const_iterator it = a_list.data_.begin(); it != a_list.data_.end(); ++it ) {
    out_stream << "\t" << *it << endl;
  }
  return out_stream;
}

And here's an example of it's use:

#include 

int main( int argc, char **argv ) {
  MyList< int > my_ints;
  my_ints.add( 1 );
  my_ints.add( 2 );

  cout << "myints" << endl;
  cout << my_ints << endl;

  MyList< string > my_strings;
  my_strings.add( "hello" );
  my_strings.add( "world" );

  cout << "mystrings" << endl;
  cout << my_strings << endl;

  return 0;
}

The output:

$ ./to_ostream 
myints
2 items:
        1
        2

mystrings
2 items:
        hello
        world