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' );
}
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';
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 << ')';
}
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 astruct
,union
,class
, orenum
). - 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";
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";
This works because:
- The
indent(4)
is a constructor call rather than a function call that creates a temporary object to remember the indentation amount. - The overloaded
<<
will then “print” the object by usingsetw()
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) asconst
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";
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 ofvoid*
, 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 thelong
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 );
}
that gets a reference to the current indentation. Some notes:
The class
ios_base
is used because it’s the base class forostream
. We use it rather thanostream
becauseios_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 whichiword(index)
refers is guaranteed to be initialized to0
.
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;
}
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)