Comment implémenter une journalisation pratique sans Singleton?

Ma mise en œuvre actuelle, simplifiée:

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

En général, je suis satisfait de cette implémentation car:

  • instanciation paresseuse signifie que je ne paie pas sauf si je l’utilise
  • utilisation de unique_ptr signifie un nettoyage automatique pour que valgrind soit heureux
  • mise en œuvre relativement simple et facile à comprendre

Cependant, les points négatifs sont:

  • les singletons ne sont pas propices aux tests unitaires
  • dissonance dans le fond de mon esprit pour avoir introduit un pseudo-global (une petite odeur de code)

Voici donc mes questions adressées aux développeurs qui réussissent à exorciser tous les singletons de leur code C ++ :

  • Quel type d’implémentation non Singleton utilisez-vous pour la journalisation à l’échelle de l’application?
  • L’interface est-elle aussi simple et accessible qu’un appel Log :: LogMsg () ci-dessus?

Je souhaite éviter de faire passer une instance de journal dans tout mon code, si possible – Remarque: je pose la question car, moi aussi, je veux exorciser tous les singletons de mon code s’il existe une bonne alternative raisonnable.

Premièrement: l’utilisation de std::unique_ptr n’est pas nécessaire:

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

Produit exactement la même sémantique d’initialisation et de nettoyage paresseux sans introduire tout le bruit de syntaxe (et le test redondant).

Maintenant c’est hors de propos …

Votre cours est extrêmement simple. Vous voudrez peut-être créer une version légèrement plus compliquée. La configuration requirejse pour les messages de journal est la suivante:

  • horodatage
  • niveau
  • fichier
  • ligne
  • une fonction
  • nom du processus / ID du thread (si pertinent)

sur le message lui-même.

En tant que tel, il est parfaitement envisageable de disposer de plusieurs objects avec des parameters différents:

 // 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::ssortingng const& message, char const* function, char const* file, int line); private: Level _level; LogSink& _sink; }; 

Et vous encapsulez généralement l’access dans une macro pour plus de commodité:

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

Maintenant, nous pouvons créer un simple logger prolixe:

 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 

Et utilisez-le commodément:

 int foo(int a, int b) { int result = a + b; LOG_DEBUG("a = " << a << ", b = " << b << " --> result = " << result) return result; } 

Le but de ce coup de gueule? Tout ce qui constitue un besoin mondial ne doit pas être unique . Le caractère unique de Singletons est généralement inutile.

Note: si le peu de magie impliquant std::ossortingngstream vous effraie, c'est normal, voir cette question

Je choisirais la solution simple et pragmatique:

vous voulez une solution globalement accessible. Pour la plupart, j’essaie d’éviter les globals, mais pour les enregistreurs, avouons-le, c’est généralement peu pratique.

Nous avons donc besoin que quelque chose soit mondialement accessible.

Mais nous ne voulons pas de la ressortingction supplémentaire “il ne peut y avoir qu’une seule” conférée par un singleton. Certains de vos tests unitaires peuvent vouloir instancier leur propre enregistreur privé. D’autres voudront peut-être remplacer l’enregistreur global.

Alors faites-en un global. Une simple vieille variable globale simple.

Cela ne résout toujours pas complètement le problème des tests unitaires, certes, mais nous ne pouvons pas toujours avoir tout ce que nous voulons. 😉

Comme indiqué dans le commentaire, vous devez prendre en compte l’ordre d’initialisation des globals, qui, en C ++, est partiellement indéfini.

Dans mon code, ce n’est généralement pas un problème, car j’ai rarement plus d’un global (mon enregistreur), et je m’en tiens rigoureusement à la règle de ne jamais permettre aux globaux de dépendre les uns des autres .

Mais c’est quelque chose que vous devez au moins considérer.

J’aime beaucoup l’interface suivante, car elle utilise le streaming. Bien sûr, vous pouvez y append des chaînes, du temps et des informations de fil. Une autre extension possible consiste à utiliser les macros __FILE__ et __LINE__ et à les append en tant que parameters au constructeur. Vous pouvez même append une fonction de modèle variadique si vous n’aimez pas la syntaxe du stream. Si vous souhaitez stocker certaines configurations, vous pouvez les append à des variables statiques.

 #include  #include  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  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; } 
 // file ILoggerImpl.h struct ILoggerImpl { virtual ~ILoggerImpl() {} virtual void Info(std::ssortingng s) = 0; virtual void Warning(std::ssortingng s) = 0; virtual void Error(std::ssortingng s) = 0; }; // file logger.h // #include "ILoggerImpl.h" class CLogger: public ILoggerImpl { public: CLogger():log(NULL) { } //interface void Info(std::ssortingng s) {if (NULL==log) return; log->Info(s); } void Warning(std::ssortingng s) {if (NULL==log) return; log->Warning(s); } void Error(std::ssortingng 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: Ah // #include "loggers.h" class A { public: void foo() { Log1.Info("asdhoj"); Log2.Info("asdhoj"); Log3.Info("asdhoj"); } private: }; /// file: Bh // #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 "Ah" #include "Bh" #include "fileloger.h" #include "xmllogger.h" CLogger Log1; CLogger Log2; CLogger Log3; CLogger Log4; CLogger LogB; // client code int main() { std::unique_ptr filelog1(new CFileLogger("C:\\log1.txt")); Log1.BindImplementation(*filelog1.get()); std::unique_ptr xmllogger2(new CXmlLogger("C:\\log2.xml")); Log2.BindImplementation(*xmllogger2.get()); std::unique_ptr 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 "Ah" TEST(test1) { A a; } TEST(test2, A_logs_to_Log1_when_foo_is_called()) { A a; std::unique_ptr filelog1Mock(new CFileLoggerMock("C:\\log1.txt")); Log1.BindImplementation(*filelog1.get()); EXPECT_CALL(filelog1Mock Info...); a.foo(); Log1.UnbindImplementation(); }