如何在没有Singleton的情况下实现方便的日志记录?

时间:2011-12-01 06:19:21

标签: c++ singleton c++11

我目前的实施,简化:

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

一般来说,我对此实现感到满意,因为:

  • 懒惰的实例化意味着我不付钱,除非我使用它
  • 使用unique_ptr意味着自动清理所以valgrind很高兴
  • 相对简单,易于理解的实施

然而,负面影响是:

  • 单身人士不利于单元测试
  • 在我的脑海中引入假全局(一点代码气味)的不和谐

所以这里是我的问题针对那些成功驱逐C ++代码中所有单例的开发人员

  • 您在应用程序范围内进行日志记录时使用哪种非单例实现?
  • 界面是否像上面的Log :: LogMsg()调用一样简单易用?

我想避免在我的代码中传递一个Log实例,如果可能的话 - 请注意:我问,因为,如果有一个好的,合理的选择,我也想从代码中驱除所有Singletons。< / p>

4 个答案:

答案 0 :(得分:40)

首先:不需要使用std::unique_ptr

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

生成完全相同的延迟初始化和清理语义,而不会引入所有语法噪声(和冗余测试)。

现在已经不在了......

你的课非常简单。您可能希望构建稍微复杂的版本,日志消息的典型要求是:

  • 时间戳
  • 水平
  • 文件
  • 线
  • 功能
  • 进程名称/线程ID(如果相关)

在邮件本身之上。

因此,完全可以设想具有不同参数的多个对象:

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

为方便起见,您通常将访问权限包装在宏中:

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

现在,我们可以创建一个简单的详细记录器:

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

方便使用:

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

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

这个咆哮的目的?并非所有这些都需要唯一。单身人士的独特性通常是无用的。

注意:如果涉及std::ostringstream的魔法让您感到害怕,这是正常现象,see this question

答案 1 :(得分:12)

我会选择简单实用的解决方案:

您需要一个全局可访问的解决方案。在大多数情况下,我尽量避免使用全局变量,但对于记录器,让我们面对它,这通常是不切实际的。

因此,我们确实需要 来全局访问。

但是,我们不希望单身人士给予额外的“只有一个”限制。您的某些单元测试可能希望实例化自己的私有记录器。其他人可能想要替换全局记录器。

所以让它成为全球性的。一个普通的简单全局变量。

诚然,这仍然没有完全解决单元测试的问题,但我们不能总是拥有我们想要的一切。 ;)

正如评论中所指出的,你需要考虑全局变量的初始化顺序,在C ++中,它是部分未定义的。

在我的代码中,这通常不是问题,因为我很少有一个以上的全局(我的记录器),并且我严格遵守从不允许全局变量依赖彼此的规则

但至少你必须考虑这件事。

答案 2 :(得分:11)

我非常喜欢以下界面,因为它使用流媒体。当然,您可以添加频道,时间和线程信息。另一种可能的扩展是使用__FILE____LINE__宏并将其作为参数添加到构造函数中。如果您不喜欢流语法,甚至可以添加可变参数模板函数。如果要存储某些配置,可以将它们添加到某些静态变量中。

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

答案 3 :(得分:0)

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