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:
Cependant, les points négatifs sont:
Voici donc mes questions destinées aux développeurs qui réussissent à exorciser tous les singletons de leur code C++ :
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.
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:
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
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.
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;
}
// 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();
}