10

I'm working on a small C++11 application (an SDL2 game) and i'm having a hard time "porting" some of my object-oriented knowledge from PHP/Java to C++. For example, in order to create an elegant error logging approach, i would create a class with various adapters and centralize logging there. I already did that in C++, but i have no idea on how my classes should be using the Logger class.

In Java and PHP, i would use dependency injection, and put the Logger as a class member variable in them. But in C++, what's the proper way? I don't really think that going static would be nice.

vinnylinux
  • 7,050
  • 13
  • 61
  • 127
  • "In Java and PHP, i would use dependency injection, and put the Logger as a class member variable in them." What a coincidence, I would do that in C++! – Bret Kuhns Oct 03 '13 at 17:53
  • The usual question comes up, is there a good reason that you're reinventing the wheel, rather than using an existing logging library (e.g. log4cxx or whatever suits you best)? To clarify, I'm not saying that you should've write your own logging classes, I'm just wondering why and if you considered the alternatives. – thelamb Oct 03 '13 at 17:56
  • So you like class member variables in Java but you don't like static member variables in C++? – rodrigo Oct 03 '13 at 17:56

4 Answers4

7

Oh man.

To me logging is similar to date/time handling: the basic case is trivial, but anything more than trivial is extremely complicated: no middle ground.

Let me advise you to look into a general purpose logging library such as Pantheios or Boost.Log.

The reason why I advice for this approach as opposed to making "your own effort", is that I know first hand how the "logging situation" goes:

  • you start with a simple "write to file" or "write to screen"
  • then you need to also log to another device
  • then you want to filter out severity levels
  • then you want to send your logs via pipes
  • then you want to turn off logging

And it all becomes very, very difficult, and the logging classes start polluting your code.

So, like I said: based on my limited experience, I would encourage you to look into the suggested libraries.

Good luck.

Edit: Boost.Log examples

Just for completeness of the post (refer to page for details).

Trivial case:

#include <boost/log/trivial.hpp>
int main(int, char*[]) {
    BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
    BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
    BOOST_LOG_TRIVIAL(info) << "An informational severity message";
    BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
    BOOST_LOG_TRIVIAL(error) << "An error severity message";
    BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

    return 0;
}
Escualo
  • 40,844
  • 23
  • 87
  • 135
2

One approach would be to pass a reference to a logger object around function calls. However, logging is a sort of an orthogonal aspect to application logic, so that explicitly passing that logger and having it as a member quickly becomes a nuisance and only adds artificial complexity.

I prefer having one global logger in the application. Modules can create its own loggers as child loggers of the main logger forming a hierarchy (I think this is similar to Python logging module) and control its output sink and verbosity independently if necessary.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
2

I always use something like this:

class Log
{
public:
    Log()
        : m_filename( "dafault.log" )
    {}

    // if you wanna give other names eventually...
    Log( const std::string & p_filename )
        : m_filename( p_filename )
    {}

    virtual ~Log()
    {
        // implement  your writeToFile() with std::ofstream 
        writeToFile( m_filename, m_stream, true );
    } 

    template< typename T >
    Log & operator<<( const T & p_value )
    {
        m_stream << p_value;
        return *this;
    }

private:
    std::string         m_filename;
    std::ostringstream  m_stream;
};

So this way I am able to log like this:

Log() << "My message in the log with numbers " << 1 << 2 << 3 << " and so on...";

Log( "other.log" ) << "Log in another file eventually...";
Wagner Patriota
  • 5,494
  • 26
  • 49
0

My current approach is to use a kind of dependency injection, using C++ strengths instead of magic. It does not require on anything specific to C++11 (except that __thread which is an extension could be replaced by thread_local if you wished to be Standard).

class LoggerEngine {
public:
    static LoggerEngine* Current() { return CurrentE; }

    virtual bool isActive(Level) { return true; }

    virtual void log(char const* function,
                     char const* file,
                     int line,
                     std::string message) = 0;

    // cuz' decorators rock
    LoggerEngine* previous() const { return _previous; }

protected:
    LoggerEngine(): _previous(CurrentE) { CurrentE = this; }
    ~LoggerEngine() { CurrentE = _previous; }

private:
    static __thread LoggerEngine* CurrentE;

    LoggerEngine* const _previous;
}; // class LoggerEngine

// in some .cpp file:
__thread LoggerEngine* LoggerEngine::CurrentE = 0;

And then, provide macros (to capture function, file and line):

#define LOG(L_, Message_)                                                     \
    do { if (LoggerEngine* e = LoggerEngine::Current() and e->isActive(L_)) { \
        std::ostringstream _28974986589657165;                                \
        _28974986589657165 << Message_;                                       \
        e->log(__func__, __FILE__, __LINE__, _28974986589657165.str());       \
    }} while(0);

However it could certainly be made better by using shims instead, because even though it prevents any computation in case the level is not active it still requires formatting of the full message (and the necessary memory allocation) even if it is going to truncate the message anyway (for example because it uses fixed-size buffers) and does not easily allow customization of the formatting.

The combination of stacking engines (and popping them off automatically using RAII) with thread-local behavior is really pretty neat. Most code only ever see an interface, without having to thread it by (cool when you have 4/5 different engines), and any level of the stack can switch the engine to something more appropriate.

There is one caveat, as is, no logging occurs before a first Engine is defined. For this reason I've often thought of defaulting to writing to the console if no engine is setup but... I've mostly changed my style to avoid computation before main is called since I cannot dependency-inject during this phase (and it's awkward if an exception fires...)


Usage is like this:

 void benchmark() {
     LOG(INFO, "Hello, World!");

     Timer t;
     {
         MySinkLogger const _; (void)_; // a logger with "isActive" always false
         for (size_t i = 0; i != 10000; ++i) {
             LOG(INFO, "Flood!");
         }
     }
     LOG(INFO, "Elapsed: " << t.elapsed());
 }

 int main() {
     MyFileLoggerEngine const _("somefile.log"); (void)_; // a file logger

     benchmark();
 }

And normally this could create a file "somefile.log" containing:

2013-10-03T18:38:04.645512 mylaptop INFO <test.cpp#42> Hello, World!
2013-10-03T18:38:04.865765 mylaptop INFO <test.cpp#47> Elapsed: 0.220213s
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722