40

My current implementation, simplified:

#include <string>
#include <memory>

class Log
{
  public:
    ~Log() {
      // closing file-descriptors, etc...
    }
    static void LogMsg( const std::string& msg )
    {
      static std::unique_ptr<Log> g_singleton;
      if ( !g_singleton.get() )
        g_singleton.reset( new Log );
      g_singleton->logMsg( msg );
    }
  private:
    Log() { }
    void logMsg( const std::string& msg ) {
      // do work
    }
};

In general, I am satisfied with this implementation because:

  • lazy instantiation means I don't pay unless I use it
  • use of unique_ptr means automatic cleanup so valgrind is happy
  • relatively simple, easy-to-understand implementation

However, the negatives are:

  • singletons aren't conducive to unit-testing
  • dissonance in the back of my mind for introducing a pseudo-global (a bit of a code smell)

So here are my questions directed towards those developers who are successful in exorcising all singletons from their C++ code:

  • What kind of non-Singleton implementation do you use for application-wide logging?
  • Is the interface as simple and accessible as a Log::LogMsg() call above?

I want to avoid passing a Log instance all over my code, if at all possible - note: I am asking because, I, too, want to exorcise all Singletons from my code if there is a good, reasonable alternative.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
kfmfe04
  • 14,936
  • 14
  • 74
  • 140
  • 1
    Why do you want to avoid Singleton here? It is a good case for Singleton! Or Have a static Log member in your Log class. – Basile Starynkevitch Dec 01 '11 at 06:21
  • Look at the negatives I listed above - however, ty for your input - makes me feel less guilty for using a Singleton... – kfmfe04 Dec 01 '11 at 06:23
  • 1
    I don't think cognitive dissonance counts as a negative... Can you provide a source for why "singletons aren't conducive to unit-testing"? – Will Bickford Dec 01 '11 at 06:30
  • 1
    Possible reference: http://c2.com/cgi/wiki?UnitTestingSingletons – Will Bickford Dec 01 '11 at 06:31
  • 1
    @Will: Well, when singletons are used for logging, it's not a problem. But suppose the singleton had state (like a handle to Mysql), then it can cause issues. However, if I undo the state-due-to-testing in a tear-down instead of in a destructor, the problem should go away. TY for making me think this through - maybe a singleton isn't so bad after all (if we don't abuse its usage). – kfmfe04 Dec 01 '11 at 06:36
  • 7
    You just need a regular global, not a singleton. – GManNickG Dec 01 '11 at 06:37
  • 8
    `+1` for trying to avoid a singleton. – sbi Dec 01 '11 at 07:05
  • Have a look at: http://stackoverflow.com/questions/8330291/implement-minimal-logging-program-in-c – Ghita Dec 01 '11 at 07:15
  • 1
    Since a Log object is a single shared data structure that is meant to be used by all of your code, a singleton/global seems like the obvious way to go. The alternative would be to pass a pointer to a Log object to *every single function in your entire codebase that might ever want to do logging*, which would avoid the code smell but would be a tremendously tedious to implement, and less efficient as well. – Jeremy Friesner Dec 01 '11 at 07:35
  • 5
    @JeremyFriesner: why you would you want to log everything through the same object ? Why cannot you have different objects depending on the kind of the log, the formatting requirements, the destination (rotated file, UDP stream, console, ...) ? Granted, logging is usually implemented with globals because it is *simpler*, it does not make it necessary or efficient though. – Matthieu M. Dec 01 '11 at 07:40
  • @MatthieuM. I think it depends on what's on the other side of the Log - if it's a file or database or some other slow I/O, not flushing, closing, or constructing for every single call may, indeed, be more efficient. But I agree with your "not exactly necessary" assessment. – kfmfe04 Dec 01 '11 at 07:52
  • 13
    @JeremyFriesner: no, I'm sorry to hear that you have been brainwashed so by the design patterns mafia, but the purpose of a singleton is not to "make something available to all of your code". That is what a *global* is for. – jalf Dec 01 '11 at 08:11
  • 4
    @jalf +1 for _design patterns mafia_. I lol'd – Gunther Piez Oct 11 '12 at 10:05
  • This is still a singleton, maybe somewhat hidden, but using a static variable in a function is the essence of a Meyers singleton. A static in a function is global state: it persists over different unit test cases in the same executable, so it has the exact same issues. (because imho it is just a variation of a singleton). A singleton is a somewhat safer way to use a global, as using the global directly can lead to threading issues or static initialization order fiasco. A static variable in a function is initialized when the function is called, but it lives in the data section of the binary. – Emile Vrijdags May 22 '23 at 16:55

4 Answers4

45

First: the use of std::unique_ptr is unnecessary:

void Log::LogMsg(std::string const& s) {
  static Log L;
  L.log(s);
}

Produces exactly the same lazy initialization and cleanup semantics without introducing all the syntax noise (and redundant test).

Now that is out of the way...

Your class is extremely simple. You might want to build a slightly more complicated version, typical requirements for log messages are:

  • timestamp
  • level
  • file
  • line
  • function
  • process name / thread id (if relevant)

on top of the message itself.

As such, it is perfectly conceivable to have several objects with different parameters:

// LogSink is a backend consuming preformatted messages
// there can be several different instances depending on where
// to send the data
class Logger {
public:
  Logger(Level l, LogSink& ls);

  void operator()(std::string const& message,
                  char const* function,
                  char const* file,
                  int line);

private:
  Level _level;
  LogSink& _sink;
};

And you usually wrap the access inside a macro for convenience:

#define LOG(Logger_, Message_)                  \
  Logger_(                                      \
    static_cast<std::ostringstream&>(           \
      std::ostringstream().flush() << Message_  \
    ).str(),                                    \
    __FUNCTION__,                               \
    __FILE__,                                   \
    __LINE__                                    \
  );

Now, we can create a simple verbose logger:

Logger& Debug() {
  static Logger logger(Level::Debug, Console);
  return logger;
}

#ifdef NDEBUG
#  define LOG_DEBUG(_) do {} while(0)
#else
#  define LOG_DEBUG(Message_) LOG(Debug(), Message_)
#endif

And use it conveniently:

int foo(int a, int b) {
  int result = a + b;

  LOG_DEBUG("a = " << a << ", b = " << b << " --> result = " << result)
  return result;
}

The purpose of this rant ? Not all that is a global need be unique. The uniqueness of Singletons is generally useless.

Note: if the bit of magic involving std::ostringstream scares you, this is normal, see this question

Community
  • 1
  • 1
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 2
    +1 for noticing that a unique_ptr<> is not needed here. The chunk in the OP was transcribed from my real code where the actual logger could be a subclass of Logger: in that case, having an unique_ptr<> is useful for cleanup. – kfmfe04 Dec 01 '11 at 08:09
  • @kfmfe04 the static variable used here is destructed as well at the end of the program. – David Feurle Dec 01 '11 at 08:14
  • @DavidFeurle: the point is that the OP instanciated a subclass of `Logger` and thus needed polymorphic cleanup. Another scheme could have been having several functions with local `static` variables and initializing a reference to a `Logger&` in the main function. – Matthieu M. Dec 01 '11 at 08:17
  • 3
    @MatthieuM.: Just picking at silly stuff now, but I would think it's more common to leave the semicolon out of the macro, so the use of the `LOG_DEBUG` macro looks like a statement in client code (and not inferred to be). – GManNickG Dec 01 '11 at 09:03
  • @GMan: I am wary of passing macros as functions, notably because forgetting the semi-colon might then lead to very confusing messages for the client exposing the macro internals (in general). I prefer to put it in, and the client is free to add one of her own if it pleases her (it just won't have any effect) and won't be punished if she does not. It's quite subjective though, I guess. – Matthieu M. Dec 01 '11 at 09:22
  • How should the Level and the LogSink look like? – JavaRunner Apr 12 '16 at 03:23
  • 1
    @JavaRunner: `Level` would be an `enum` such as `enum class Level: uint8_t { Fatal, Critical, Error, Warning, Notification, Information, Debug }` and the `LogSink` would be some kind of interface with a `write` method. – Matthieu M. Apr 12 '16 at 06:08
  • @JavaRunner: It was an example constant of a type implementing `LogSink` meant to represent the usual stdout. – Matthieu M. Apr 12 '16 at 07:05
13

I'd go with the simple, pragmatic solution:

you want a solution that is globally accessible. For the most part, I try to avoid globals, but for loggers, let's face it, it's usually impractical.

So, we do need something to be globally accessible.

But, we don't want the additional "there can be only one" restriction that a singleton confers. Some of your unit tests might want to instantiate their own private logger. Others might want to replace the global logger, perhaps.

So make it a global. A plain old simple global variable.

This still doesn't fully solve the problem with unit testing, admittedly, but we can't always have everything we want. ;)

As pointed out in the comment, you need to consider the initialization order for globals, which, in C++, is partly undefined.

In my code, that is generally not a problem, because I rarely have more than one global (my logger), and I stick rigidly to a rule of never allowing globals to depend on each others.

But it's something you have to consider, at least.

jalf
  • 243,077
  • 51
  • 345
  • 550
  • 1
    I would still recommend a local static even for a global variable. Initialization Order Fiasco and all... after all loggers can be used during the globals instantiation. – Matthieu M. Dec 01 '11 at 08:18
  • @MatthieuM. true enough. Appended a note to my answer dealing with that bit. – jalf Dec 01 '11 at 08:30
11

I really like the following interface since it uses streaming. Of course you can add channels, time and thread information to it. Another possible extension is to use the __FILE__ and __LINE__ macros and add it as parameters to the constructor. You could even add a variadic template function if you do not like the stream syntax. If you want to store some configuration you could add them to some static variables.

#include <iostream>
#include <sstream>

class LogLine {
public:
    LogLine(std::ostream& out = std::cout) : m_Out(out) {}
    ~LogLine() {
        m_Stream << "\n";
        m_Out << m_Stream.rdbuf();
        m_Out.flush();
    }
    template <class T>
    LogLine& operator<<(const T& thing) { m_Stream << thing; return *this; }
private:
    std::stringstream m_Stream;
    std::ostream& m_Out;
    //static LogFilter...
};

int main(int argc, char *argv[])
{
    LogLine() << "LogLine " << 4 << " the win....";
    return 0;
}
David Feurle
  • 2,687
  • 22
  • 38
  • +1 for nice use of template function in conjunction with streaming – kfmfe04 Dec 01 '11 at 07:33
  • 3
    If you want to use `__FILE__` and `__LINE__` you need to use macros, or pass them explicitly. Putting that in the constructor definition will always give you the file and line where the constructor is. Also beware that the output in this example is not thread-safe: another thread could push their output before this one pushes `std::endl`, and you'd end up with two merged lines. But +1 anyway for making use of the destructor for the output. That's a neat technique. – R. Martinho Fernandes Dec 01 '11 at 07:37
  • Thats what I tried to say with add the macros to the constructor. We use it like this: LogLine(LOG_DEBUG) - while LOG_DEBUG is a macro that evaluates to __FILE__ __LINE__ and the log channel. – David Feurle Dec 01 '11 at 07:45
  • @R.MartinhoFernandes: The output could be made thread safe by first putting `\n` in `m_Stream` and *then* pushing the stream into `std::cout`. My only gripes with the `stringstream` solutions (that I use on pet projects for their convenience) is the double punition you take: it would be so much better if `str` could return by reference and not by copy :( – Matthieu M. Dec 01 '11 at 08:00
  • @MatthieuM. thanks for pointing the copy issue out. I changed the example to use rdbuf - should avoid the copy. – David Feurle Dec 01 '11 at 08:06
  • @DavidFeurle: even better `~LogLine() { m_Stream << '\n'; std::cout << m_Stream.rdbuf(); }` should avoid the data race outlined by R. Martinho Fernandes. (and thanks for pointing out that one could directly stream the stream buffer, never occurred to me!) – Matthieu M. Dec 01 '11 at 08:15
  • @Matthiew I think you should still flush. You don't want to lose logs because the apocalypse happened. Granted the same race could happen, but it's not going to hurt now, because the lines won't get merged. – R. Martinho Fernandes Dec 01 '11 at 08:17
  • why downvote? Isn't it a correct answer? :S Could you please leave a comment when downvoting? – David Feurle Dec 01 '11 at 08:43
  • made this a logging library: http://sourceforge.net/projects/streamlogger/ or http://spore.sodgeit.de/sporeblogarticles/articles/Blog-Streamlogger.html – David Feurle Feb 27 '12 at 09:17
  • The thing that I don't like about this solution, is that it only logs on destruction of the LogLine object. This is bad if you have code that has an opening log line, then enters a loop where every iteration is logged. The log lines in the loop get printed before any log lines before the loop, because they have gone out of scope first. How can this be adapted to print immediately? – rem45acp Dec 06 '18 at 15:46
  • @rem45acp the logline instance is not a named variable. This is named a temporary variable. The destructor of a temporary is called at the end of the statement - aka the next semicolon. This means the lines are logged in a strict order. – David Feurle Oct 27 '20 at 13:11
-2
// file ILoggerImpl.h 

struct ILoggerImpl
{
    virtual ~ILoggerImpl() {}
    virtual void Info(std::string s) = 0;
    virtual void Warning(std::string s) = 0;
    virtual void Error(std::string s) = 0;
};


// file logger.h //
#include "ILoggerImpl.h"

class CLogger: public ILoggerImpl
{
public:
    CLogger():log(NULL) {  }

    //interface
    void Info(std::string s)  {if (NULL==log) return; log->Info(s); }
    void Warning(std::string s) {if (NULL==log) return; log->Warning(s); }
    void Error(std::string s) {if (NULL==log) return; log->Error(s); }


    //
    void BindImplementation(ILoggerImpl &ilog) { log = &ilog; }
    void UnbindImplementation(){ log = NULL; }


private:
    ILoggerImpl *log;
};


// file: loggers.h //

#include "logger.h"
extern CLogger Log1;
extern CLogger Log2;
extern CLogger Log3;
extern CLogger Log4;
extern CLogger LogB;



/// file: A.h //
#include "loggers.h"  

class A
{

public:
    void foo()
    {
        Log1.Info("asdhoj");
        Log2.Info("asdhoj");
        Log3.Info("asdhoj");

    }
private:

};


/// file: B.h //
#include "loggers.h"

class B
{

public:
    void bar()
    {
        Log1.Info("asdhoj");
        Log2.Info("asdhoj");
        LogB.Info("asdhoj");
        a.foo();
    }



private:

    A a;
};



////// file: main.cpp  ////////////////


#include "loggers.h"
#include "A.h"
#include "B.h"
#include "fileloger.h"
#include "xmllogger.h"

CLogger Log1;
CLogger Log2;
CLogger Log3;
CLogger Log4;
CLogger LogB;

// client code

int main()
{
    std::unique_ptr<ILoggerImpl> filelog1(new CFileLogger("C:\\log1.txt"));
    Log1.BindImplementation(*filelog1.get());

    std::unique_ptr<ILoggerImpl> xmllogger2(new CXmlLogger("C:\\log2.xml"));
    Log2.BindImplementation(*xmllogger2.get());

    std::unique_ptr<ILoggerImpl> xmllogger3(new CXmlLogger("C:\\logB.xml"));
    LogB.BindImplementation(*xmllogger3.get());


    B b;
    b.bar();



    return 0;
};



// testing code
///////file: test.cpp /////////////////////////////////

#include "loggers.h"
CLogger Log1;
CLogger Log2;
CLogger Log3;
CLogger Log4;

int main()
{
    run_all_tests();
}



///////file: test_a.cpp /////////////////////////////////

#include "A.h"

TEST(test1)
{
    A a;
}

TEST(test2, A_logs_to_Log1_when_foo_is_called())
{
    A a;
    std::unique_ptr<ILoggerImpl> filelog1Mock(new CFileLoggerMock("C:\\log1.txt"));
    Log1.BindImplementation(*filelog1.get());
    EXPECT_CALL(filelog1Mock  Info...);

    a.foo();
    Log1.UnbindImplementation();
}
Community
  • 1
  • 1