Jak zaimplementować wygodne logowanie bez Singletonu?

Moja obecna implementacja, uproszczona:

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

Ogólnie jestem zadowolony z tej realizacji, ponieważ:

  • Lazy instantiation oznacza, że nie płacę, chyba że z niego skorzystam
  • użycie unique_ptr oznacza automatyczne czyszczenie, więc valgrind jest szczęśliwy
  • stosunkowo prosta, łatwa do zrozumienia implementacja

Jednak negatywy to:

  • singletony nie sprzyjają testom jednostkowym
  • dysonans z tyłu mojego umysłu za wprowadzenie a pseudo-global (a bit of a code smell)

Oto moje pytania skierowane do tych programistów, którym udało się egzorcyzmować wszystkie singletony z kodu C++ :

  • jakiego rodzaju implementacji nie Singleton używasz do logowania w całej aplikacji?
  • czy interfejs jest tak prosty i dostępny jak wywołanie Log::LogMsg() powyżej?

Chcę uniknąć przekazywania instancji logu w całym moim kodzie, jeśli to w ogóle możliwe - Uwaga: pytam, bo ja też chcę wypędzić wszystkie Singletony z mojego kodu, jeśli istnieje dobra, rozsądna alternatywa.

Author: Rakete1111, 2011-12-01

4 answers

Po Pierwsze: użycie std::unique_ptr jest niepotrzebne:

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

Tworzy dokładnie tę samą leniwą semantykę inicjalizacji i czyszczenia bez wprowadzania szumów składni (i zbędnych testów).

To jest z drogi... Twoja klasa jest niezwykle prosta. Możesz chcieć zbudować nieco bardziej skomplikowaną wersję, typowe wymagania dla komunikatów dziennika są:
  • timestamp
  • poziom
  • plik
  • linia
  • funkcja
  • Nazwa procesu / ID wątku (jeśli dotyczy)

Na górze samej wiadomości.

Jako takie, jest doskonale możliwe posiadanie kilku obiektów o różnych parametrach:

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

I zwykle zawijasz dostęp wewnątrz makra dla wygody:

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

Teraz możemy stworzyć prosty 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

I użyj go wygodnie:

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

  LOG_DEBUG("a = " << a << ", b = " << b << " --> result = " << result)
  return result;
}
Cel tej gadki ? Nie wszystko, co jest globalne, musi być unikalne . Wyjątkowość singletonów jest na ogół bezużyteczna.

Uwaga: jeśli odrobina magii z udziałem std::ostringstream cię przeraża, jest to normalne, zobacz to pytanie

 39
Author: Matthieu M.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:09:40

Wybrałbym proste, pragmatyczne rozwiązanie:

Chcesz rozwiązania, które będzie dostępne globalnie. W większości staram się unikać globali, ale dla drwali, spójrzmy prawdzie w oczy, to zwykle niepraktyczne.

Potrzebujemy więc czegoś , aby było dostępne globalnie.

Ale nie chcemy dodatkowego ograniczenia" może być tylko jedno", które daje singleton. Niektóre z Twoich testów jednostkowych mogą chcieć utworzyć własny prywatny rejestrator. Inni mogą chcieć zastąpić może global logger.

Więc zrób to globalnie. Zwykła, prosta zmienna globalna.

To nadal nie rozwiązuje w pełni problemu z testami jednostkowymi, co prawda, ale nie zawsze możemy mieć wszystko, czego chcemy. ;)

Jak zaznaczono w komentarzu, należy wziąć pod uwagę kolejność inicjalizacji dla globali, która w C++ jest częściowo niezdefiniowana.

W moim kodzie to generalnie nie jest problem, bo rzadko mam więcej niż jeden globalny (mój logger), i trzymam się sztywno zgodnie z zasadą nigdy nie pozwalającą globalom polegać na sobie nawzajem .

Ale to jest coś, co trzeba wziąć pod uwagę, przynajmniej.

 12
Author: jalf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-06 19:04:31

Bardzo podoba mi się poniższy interfejs, ponieważ używa streamingu. Oczywiście możesz dodać do niego kanały, czas i informacje o wątku. Innym możliwym rozszerzeniem jest użycie makr __FILE__ i __LINE__ i dodanie ich jako parametrów do konstruktora. Możesz nawet dodać zmienną funkcję szablonu, jeśli nie podoba Ci się składnia strumienia. Jeśli chcesz zapisać jakąś konfigurację, możesz dodać ją do niektórych zmiennych statycznych.

#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;
}
 11
Author: David Feurle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-01 08:26:00
// 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();
}
 0
Author: user3494386,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-06 19:50:00