Note: This entry has been restored from old archives.
All these years I’ve been making regular use of good old std::endl
without realising that it is a templated function! We live in a crazy world. Somewhere in the back of my head I guess I’d just assumed it was simply platform-dependant constant… not quite.
I came across this merry discovery when implementing a logger, since re-inventing the wheel is always so much fun. It isn’t as bad as it seems, the “logger” is basically just a matter of encapsulating the relevant utility code, weighing in at less than 500 lines of which more than half are API comments.
Anyway, I’d like my logger to act like a std::ostream
, although I don’t want it to be a std::ostream
really (preferring to avoid inheriting from such beasts without a really good reason.) The functionality is simple, it wraps a set of std::ostream
s to send the output to. The important part is that it implements log levels and and log message classes (which you can register and name if you wish.) At any one time there is a log threshold and a set of active classes. To keep things really simple it has a single output
method that takes only a std::string ref.
Now I want to use it with good old <<
since that is comfortable and not unexpected so I implement:
template <typename T> Log& operator<<(Log & log, T const & t) { std::ostringstream oss; oss << t; log.output(oss.str()); return log; }
This seems to work swimmingly until I try the likes of log << std::endl
, then it fails to compile. Eh, what on earth is std::endl
that the template won’t catch it? A peak into the ostream
C++ header tells us it is a function pointer:
template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>&. endl(basic_ostream<_CharT, _Traits>& __os) { return flush(__os.put(__os.widen('n'))); }
Egads! So I implement this (bear with me):
Log& Ephedrine::operator<<(Log & log, std::ostream& (*fn)(std::ostream&)) { std::ostringstream oss; fn(oss); log.output(oss.str()); return log; }
And now I can do this:
g_log << Log::NORMAL << "Normal log output. A number is " << 2.123 << std::endl; g_log << Log::VERBOSE << "Verbose log output." << std::endl;
Joy.
But there’s more than one worm in this can. Note that in the definition above it calls flush
on the ostream
. In the doxygen comments it notes “This manipulator is often mistakenly used when a simple newline is desired, leading to poor buffering performance.” On checking the standard there it is too, this is std::endl
by definition. I’ve never used ostreams
, thus never std::endl
, in performance critical code but I’ll have to keep this in mind if I ever do! In all of the C++ books I’ve read, a fairly large number, I don’t recall ever seeing this noted anywhere.
This brings into mind a question of design. I’ve avoided being an std::ostream
yet gone and used std::endl
, and std::endl
is supposed to flush the stream (by definition, it’s in the standard, see section 27.6.2.7.) My current implementation doesn’t flush the streams, so what should I do?
- A) Not care.
- B) Reimplement as
Log::endl
(which will ultimately callstd::endl
and thus end up flushing the streams!) - C) Create a special case specifically for
std::endl
.
I don’t like the first option, not my way of doing things by preference (alas, it has been known to happen.) The second option is easy to implement but will feel unnatural to the user (a moot point, I’m the sole user!) The third option smells “wrong,” but I already decided to do it didn’t I, and I went one step worse and broke the guarantee for std::endl
(albeit probably mostly unknown.)
Is C really enough though? No, it turns out. Aside from breaking the flush guarantee it also doesn’t go quite far enough. Think about std::hex
and similar modifiers. By converting them to a string (an empty string) before writing to the underlying std::ostream
s their meaning is lost. My original implementation above is, in essence, an abomination.
In the end I settled on E: templates. What I should have done in the first place, but avoided since I was hung up on pimpl at the time (templates and pimpl don’t mix well.)
template <typename T> void Log::output(T const & t) { ... /prep/ BOOST_FOREACH(std::ostream & os, m_osList) { os << t; } ... }
std::endl is the devil incarnate, well not incarnate since std::endl isn’t but that’s beside the point.
I remember stumbling across that it flushes the stream, and thinking to myself – Jesus, what a stupid thing for it to do.
I still told my students to use it while avoiding it myself, because I’m a bastard like that…