DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on • Edited on

Custom C++ Stream Manipulators

Introduction

To print a user-defined type in C, you typically implement a function to do it, for example:

struct point {
  int x, y;
};

void print_point( struct point const *p, FILE *f ) {
  fprintf( f, "(%d,%d)", p->x, p->y );
}

void f( struct point const *p ) {
  printf( "Origin: " );
  print_point( p, stdout );
  putchar( '\n' );
}
Enter fullscreen mode Exit fullscreen mode

One of the nice things about the C++ I/O library is its overloading of << as a “stream insertion operator” to be syntactic sugar for chaining the printing of a sequence of items including user-defined types:

std::cout << "Origin: " << p << '\n';
Enter fullscreen mode Exit fullscreen mode

To make << work for a user-defined type, you simply overload << for it.

Overloading << for a User-Defined Type

To make << work for point:

std::ostream& operator<<( std::ostream &o, point const &p ) {
  return o << '(' << p.x << ',' << p.y << ')';
}
Enter fullscreen mode Exit fullscreen mode

The recipe for a user-defined type is:

  • The first parameter is std::ostream&.
  • The second parameter is a const& to the user-defined type (which must be a struct, union, class, or enum).
  • Return the stream argument.

As a general rule, you should not print a newline — let the caller do it.

Stream Manipulators

Things like std::endl are stream manipulators, that is they “manipulate” a stream in some way, but don’t involve any other object.

To implement your own manipulator, say to begin printing in a particular color on a terminal, you can do:

inline std::ostream& red( std::ostream &o ) {
  return o << "\33[31m";
}

inline std::ostream& endcolor( std::ostream &o ) {
  return o << "\33[m";
}

void f() {
  // ...
  std::cout << red << "error" << endcolor << ": oops\n";
Enter fullscreen mode Exit fullscreen mode

The recipe for a manipulator is:

  • The only parameter is std::ostream&.
  • Return the stream argument.

Setting colors on ANSI terminals is done via SGR (Select Graphic Rendition) parameters.

Stream Manipulators with Parameters

If you want to have a manipulator like std::setw() that takes a parameter, you need a helper object. For example, to implement a manipulator to indent (print) a given number of spaces:

class indent {
public:
  explicit constexpr indent( unsigned n ) : _indent{ n } { }
private:
  unsigned const _indent;

  friend std::ostream& operator<<( std::ostream &o,
                                   indent const &i ) {
    return o << std::setw( i._indent ) << "";
  }
};

void f() {
  // ...
  std::cout << indent(4) << "hello, world!\n";
Enter fullscreen mode Exit fullscreen mode

This works because:

  1. The indent(4) is a constructor call rather than a function call that creates a temporary object to remember the indentation amount.
  2. The overloaded << will then “print” the object by using setw() to set the field width to 4 then printing the empty string will print 4 spaces (the default stream fill character) before it.

The recipe for a manipulator with parameters is:

  • Create a class having the name of the manipulator with constexpr constructor(s) that take the parameter(s) you want storing the value(s) as const data members.
  • Define a friend overloaded operator << that prints the user-defined type of the manipulator class.

Note that the compiler will very likely optimize away the temporary object.

Maintaining State

Suppose you want to expand upon indent and have the stream remember what its current indentation is as well as either increment or decrement it, for example:

std::cout << "name : {\n"
          << inc_indent
          << indent << "last: \"" << last << "\"\n"
          << indent << "first: \"" << first << "\"\n"
          << dec_indent
          << "}\n";
Enter fullscreen mode Exit fullscreen mode

It turns out that streams have the feature whereby you can associate arbitrary data with them:

  • Every stream object internally maintains an array of long for user-defined data. (It also maintains an additional array of void*, but that’s a story for another time.)

  • The xalloc() function gives you an index for your exclusive use into that array.

  • The iword() function returns a reference to the long at that index that you can use to store whatever you want — in this case, the current indentation.

Given that, we can then implement:

long& indent_of( std::ios_base &b ) {
  static int const index = ios_base::xalloc();
  return b.iword( index );
}
Enter fullscreen mode Exit fullscreen mode

that gets a reference to the current indentation. Some notes:

  • The class ios_base is used because it’s the base class for ostream. We use it rather than ostream because ios_base is all we need here.

  • Calling xalloc() is guaranteed to be thread-safe, i.e., the index it returns is guaranteed to be unique.

  • The long to which iword(index) refers is guaranteed to be initialized to 0.

Given indent_of(), we can now implement:

inline std::ostream& indent( std::ostream &o ) {
  o << std::setw( static_cast<int>( indent_of( o ) ) * 4 ) << "";
  return o;
}

inline std::ostream& inc_indent( std::ostream &o ) {
  ++indent_of( o );
  return o;
}

inline std::ostream& dec_indent( std::ostream &o ) {
  auto &o_indent = indent_of( o );
  if ( o_indent > 0 )  // ensure indent stays non-negative
    --o_indent;
  return o;
}
Enter fullscreen mode Exit fullscreen mode

In the original example, the “indent” was the number of spaces to indent. For this example, the “indent” is now the number of “indentation levels” and each level is arbitrarily scaled by 4 spaces. Alternatively, there could be a global indent_scale variable that you can set to alter the scaling. You can even have a scale per stream, but that’s more complicated and therefore a story for another time.

Conclusion

Unlike the C I/O library, the C++ I/O library is extensible for both user-defined types via operator overloading and storing user-defined data via xalloc() and iword().

Top comments (0)