web-dev-qa-db-fra.com

Comment mettre en œuvre une journalisation pratique sans Singleton?

Mon implémentation actuelle, simplifiée:

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

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

  • l'instanciation paresseuse signifie que je ne paie pas sauf si je l'utilise
  • l'utilisation de unique_ptr signifie un nettoyage automatique pour que valgrind soit content
  • 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 introduire un pseudo-global (un peu une odeur de code)

Voici donc mes questions destiné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 veux éviter de passer une instance Log partout dans mon code, si possible - note: Je demande parce que, moi aussi, je veux exorciser tous les Singletons de mon code s'il existe une bonne alternative raisonnable.

39
kfmfe04

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

void Log::LogMsg(std::string 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, les exigences typiques pour les messages de journal sont:

  • horodatage
  • niveau
  • fichier
  • ligne
  • fonction
  • nom du processus/identifiant du thread (le cas échéant)

au-dessus du message lui-même.

A ce titre, il est parfaitement envisageable d'avoir plusieurs objets avec des paramètres 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::string const& message,
                  char const* function,
                  char const* file,
                  int line);

private:
  Level _level;
  LogSink& _sink;
};

Et vous enveloppez généralement l'accès dans une macro pour plus de commodité:

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

Maintenant, nous pouvons créer un simple enregistreur détaillé:

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 cette diatribe? Tout ce qui est un besoin global n'est pas unique . Le caractère unique des singletons est généralement inutile.

Remarque: si le peu de magie impliquant std::ostringstream vous fait peur, c'est normal, voir cette question

42
Matthieu M.

J'irais avec la solution simple et pragmatique:

vous voulez une solution accessible à l'échelle mondiale. Pour la plupart, j'essaie d'éviter les globaux, mais pour les bûcherons, avouons-le, c'est généralement peu pratique.

Donc, nous avons besoin de quelque chose pour être globalement accessible.

Mais, nous ne voulons pas de la restriction supplémentaire "il ne peut y avoir qu'une seule" qu'un singleton confère. Certains de vos tests unitaires peuvent vouloir instancier leur propre enregistreur privé. D'autres pourraient vouloir remplacer l'enregistreur global, peut-être.

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

Certes, cela ne résout pas complètement le problème des tests unitaires, mais nous ne pouvons pas toujours avoir tout ce que nous voulons. ;)

Comme indiqué dans le commentaire, vous devez considérer l'ordre d'initialisation des globaux, qui, en C++, est en partie 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 à une règle de ne permettant jamais aux globaux de dépendre les uns des autres.

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

12
jalf

J'aime vraiment l'interface suivante car elle utilise le streaming. Bien sûr, vous pouvez y ajouter des informations sur les chaînes, l'heure et les fils. Une autre extension possible consiste à utiliser le __FILE__ et __LINE__ macros et ajoutez-le en tant que paramètres au constructeur. Vous pouvez même ajouter une fonction de modèle variadic si vous n'aimez pas la syntaxe de flux. Si vous souhaitez stocker une configuration, vous pouvez les ajouter à certaines variables statiques.

#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
David Feurle
// 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
user3494386