How do I implement convenient logging without a Singleton?

随声附和 提交于 2019-12-18 09:56:15

问题


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.


回答1:


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




回答2:


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.




回答3:


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;
}



回答4:


// 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();
}


来源:https://stackoverflow.com/questions/8337300/how-do-i-implement-convenient-logging-without-a-singleton

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!