16

I've a working logger class, which outputs some text into a richtextbox (Win32, C++). Problem is, i always end up using it like this:

stringstream ss;  
ss << someInt << someString;  
debugLogger.log(ss.str());

instead, it would be much more convenient to use it like a stream as in:

debugLogger << someInt << someString;

Is there a better way than forwarding everything to an internal stringstream instance? If'd do this, when would i need to flush?

bdukes
  • 152,002
  • 23
  • 148
  • 175
newgre
  • 5,245
  • 4
  • 31
  • 41

6 Answers6

36

You need to implement operator << appropriately for your class. The general pattern looks like this:

template <typename T>
logger& operator <<(logger& log, T const& value) {
    log.your_stringstream << value;
    return log;
}

Notice that this deals with (non-const) references since the operation modifies your logger. Also notice that you need to return the log parameter in order for chaining to work:

log << 1 << 2 << endl;
// is the same as:
((log << 1) << 2) << endl;

If the innermost operation didn't return the current log instance, all other operations would either fail at compile-time (wrong method signature) or would be swallowed at run-time.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • How would i handle the "flush issue", i.e. at one point i need to send everything down to my richtextbox. Should i test the incoming value for "endline"? – newgre Feb 04 '09 at 15:20
  • This is a separate question but the answer is easy: instead of using '\n', use endl! This flushes the stream automatically. However, in order to wire a TextBox up to a stream you basically need to implement your own stream buffer (look at the rdbuf method). – Konrad Rudolph Feb 04 '09 at 15:24
  • 2
    Or add your own manipulator type that mimics endl/ends, with an overload of the << operator to detect it. – Kylotan Feb 04 '09 at 16:54
  • In the stream insertion overload, instead of working with lvalue refs (log&) we could take in and then return (move) a non-const rvalue log&&. That way a single "log <<.." statement could work on the same log rvalue which would keep moving from the inside out till the whole "log << ..." expression concludes. Once that happens, the sstream built up to that point can be processed unconditionally in the log dtor without any regard to newlines or endl's. – Neelabh Mam Nov 04 '22 at 10:31
  • @NeelabhMam … Why would you do that?! Passing by rvalue reference would steal ownership from the caller, who would now have to use `std::move(log) << …`, and could no longer reuse the `log` object afterwards. That’s an all-round bad idea, and also completely unnecessary. – Konrad Rudolph Nov 04 '22 at 13:16
  • @KonradRudolph every log statement in this scheme would use a temporary (new) log object which would be destroyed at the end of a single log expression. Sorry that it was not very explicit in the comment. Yes, persistent ownership does not work here.. but logger implementations almost always vary and considering OP's original mission statement this certainly is not a bad idea. like this: log() << a << b << c; std::move will happen in the log's friend operator << so that the member sstream moves(and gets updated) across the whole chain. Once the expression ends, ~log() can process the ss. – Neelabh Mam Nov 04 '22 at 14:29
  • 1
    @NeelabhMam Ah indeed, that would work. Though the ownership-stealing behaviour of the stream insertion operator would still be unconventional and thus potentially error-prone. I think it's safer to rely on existing paradigms of flushing streams (= either explicitly, via `std::flush` and `std::endl` insertion, or implicitly by internally checking for newline characters). – Konrad Rudolph Nov 04 '22 at 14:43
16

Overloading the insertion operator<< is not the way to go. You will have to add overloads for all the endl or any other user defined functions.

The way to go is to define your own streambuf, and to bind it into a stream. Then, you just have to use the stream.

Here are a few simple examples:

Luc Hermitte
  • 31,979
  • 7
  • 69
  • 83
  • _"You will have to add overloads for all the endl or any other user defined functions."_ That's one overload. – Lightness Races in Orbit Mar 08 '18 at 15:13
  • @LightnessRacesinOrbit That's more than one overload because we need to take care of `endl`, `flush`, `hex`, `setw`, and so on. It the end, several overloadeds need to be defined. We can reduce the number of overloads thanks to templates, still, this isn't the best approach here. – Luc Hermitte Mar 08 '18 at 16:00
  • No, one overload is required for all of them. The standard library is designed that way. – Lightness Races in Orbit Mar 08 '18 at 16:31
4

As Luc Hermitte noted, there is "Logging In C++" article which describes very neat approach to solve this problem. In a nutshell, given you have a function like the following:

void LogFunction(const std::string& str) {
    // write to socket, file, console, e.t.c
    std::cout << str << std::endl;
}

it is possible to write a wrapper to use it in std::cout like way:

#include <sstream>
#include <functional>

#define LOG(loggingFuntion) \
    Log(loggingFuntion).GetStream()

class Log {
    using LogFunctionType = std::function<void(const std::string&)>;

public:
    explicit Log(LogFunctionType logFunction) : m_logFunction(std::move(logFunction)) { }
    std::ostringstream& GetStream() { return m_stringStream; }
    ~Log() { m_logFunction(m_stringStream.str()); }

private:
    std::ostringstream m_stringStream;
    LogFunctionType m_logFunction;
};

int main() {
    LOG(LogFunction) << "some string " << 5 << " smth";
}

(online demo)

Also, there is very nice solution provided by Stewart.

PolarBear
  • 1,117
  • 15
  • 24
  • Having got that far, could we not then dispense with the downstream selection of the logging function, and instead just `#define LOG() Log(LogFunction).GetStream()` so that it can be used simply as `LOG() << "some string " << 5 << " smth";`? This would seem to be similar to what Qt do for their own `qDebug()` stream and friends. (`qWarning()`, `qFatal()`, etc.) ...Though `qDebug()` et al have several other nice features beyond that — like automatic insertion of whitespace between stream arguments. Naturally, they also provide the stream manipulator `debug.nospace()` to disable it. – FeRD Sep 18 '20 at 01:41
  • 1
    @FeRD, yes you can change the code to have `LOG()` macro without parameters. You can also create another macro on top of the existing one `#define MyLog() LOG(MyLogFunction)`. Qt approach is nice, if you use Qt library I would recommend to just use their built in logging functionality. – PolarBear Sep 18 '20 at 06:20
  • Well, in my specific case I'm looking at enhancing our existing logging — which publishes messages to ØMQ, but has the clunkiest API in the world for doing it. So I figure the closer I can get it to "basically `qDebug()` but with the message bus backend", the more usable it'll be. – FeRD Sep 19 '20 at 23:57
3

An elegant solution that also solves the flushing issues is the following:

#include <string>
#include <memory>
#include <sstream>
#include <iostream>

class Logger
{
    using Stream = std::ostringstream;
    using Buffer_p = std::unique_ptr<Stream, std::function<void(Stream*)>>;

public:
    void log(const std::string& cmd) {
        std::cout << "INFO: " << cmd << std::endl;
    }

    Buffer_p log() {
        return Buffer_p(new Stream, [&](Stream* st) {
            log(st->str());
        });
    }
};

#define LOG(instance) *(instance.log())

int main()
{
    Logger logger;
    LOG(logger) << "e.g. Log a number: " << 3;
    return 0;
}
Marios V
  • 1,174
  • 9
  • 15
0

Late to the party, but one can also extend the std::ostringstream to one that logs on destruct and then use it as a 'temporary' (don't give it a name, so it destructs right away):

class StreamToLog : public std::ostringstream
{
    int level = INFO;
public:
    StreamToLog(int level);
    ~StreamToLog();
};

StreamToLog::StreamToLog(int level) :
    level(level)
{

}

StreamToLog::~StreamToLog()
{
    const std::string s = str();

    // This 'cout' is simply for demo purposes. You can do anything else
    // like invoke a standard logger.
    std::cout << levelToString(level) << " " << s << std::endl;
}

And then you can implement some function somewhere to retrieve this stream:

StreamToLog Logger::log(int level)
{
    return StreamToLog(level);
}

And then log like:

    Logger::log(DEBUG) << "Hello, I'll have " << 3 " beers.";
Halfgaar
  • 732
  • 2
  • 7
  • 32
-1

In the Logger class, override the << operator.

Click Here to know how to implement the << operator.

You can also avoid the logging statements inside the code using Aspect Oriented programming.

Vinay
  • 4,743
  • 7
  • 33
  • 43