5

i would like to create a flexible logger class. I want it to be able to output data to a file or to standard output. Also, i want to use streams. The class should look something like:

class Logger
{
private:
   std::ostream m_out; // or ofstream, iostream? i don't know
public:

   void useFile( std::string fname);
   void useStdOut();

   void log( symbol_id si, int val );
   void log( symbol_id si, std::string str );
   //etc..
};

The symbol_id is an enum and defines the formatting. What i want to achieve is to be able to easily switch from standart output to a file and vice versa (this is the purpose of the use* methods). Preferably by just using m_out and simply writing m_out << "something"; without any checks whether i want to write to a file or stdout.

I know there are many ways how to get around this (using if's to test if i want to write to a file or stdout, the "C way" (using FILE* and fprintf)) and so on, but i'm sure there must be a way how to achieve this with C++ streams in a nice way. But i can't seem to find the way how to do it. Can somebody help me please?

PeterK
  • 6,287
  • 5
  • 50
  • 86

4 Answers4

9

The std::o*stream classes in C++ inherit from std::ostream. This means you should write your interface depending on a std::ofstream pointer or reference:

class Logger
{
    std::ostream *m_out; // use pointer so you can change it at any point
    bool          m_owner;
public:
    // constructor is trivial (and ommited)
    virtual ~Logger()
    {
        setStream(0, false);
    }
    void setStream( std::ostream* stream, bool owner )
    {
        if(m_owner)
            delete m_out;
        m_out = stream;
        m_owner = owner;
    }
    template<typename T>
    Logger& operator << (const T& object)
    {
        if(!m_out)
            throw std::runtime_error("No stream set for Logger class");
        (*m_out) << object;
        return *this;
    }
};

// usage:
Logger logger;
logger.setStream( &std::cout, false ); // do not delete std::cout when finished
logger << "This will be logged to std::cout" << std::endl;
// ...
logger.setStream( 
    new std::ofstream("myfile.log", std::ios_base::ate|std::ios_base::app), 
    true ); // delete the file stream when Logger goes out of scope
logger << "This will be appended to myfile.log" << std::endl;
utnapistim
  • 26,809
  • 3
  • 46
  • 82
  • Thanks. Upvoting your answer too since it is correct, but the_mandrill posted something similar sooner, so he gets the "accepted answer" flag ;) – PeterK Jul 02 '10 at 13:08
  • @sree, neither of the two operations is threadsafe (i.e. you can send two messages to the same log in a race condition, you can send data and change the logging pointer in a race condition and you can set two different logging streams in a race condition). – utnapistim Sep 03 '14 at 09:46
  • Buggy! [Error] invalid use of incomplete type 'std::ofstream {aka class std::basic_ofstream}' and cannot pass extra parameters. – uss Sep 03 '14 at 11:12
  • @sree, For that error, `#include `. I'm not sure what you mean by "cannot pass extra parameters" (extra parameters to what? what are you trying to do?) – utnapistim Sep 03 '14 at 11:28
8

The way I've attacked this problem before is to make Logger an abstract base class and create separate FileLogger and OutStreamLogger classes. Then create a CompositeLogger object that implements the Logger interface, which just outputs all loggers:

CompositeLogger compLogger;
compLogger.Add(new FileLogger("output.txt"));
compLogger.Add(new StreamLogger(std::cout));
...
compLogger.log(...);

If you don't need this level of flexibility and want to keep all this in a single class you could make the m_Out variable a pointer to std::ostream and add an extra flag to keep track of whether you need to delete it on cleanup:

private:
  std::ostream*   m_out;
  bool            m_OwnsStream;

Logger() {
   m_Out=&std::cout; // defaults to stdout
   m_OwnsStream=false;
}
void useFile(std::string filename) {
  m_Out=new std::ofstream(filename);
  m_OwnsStream=true;
}
~Logger() {
  if (m_OwnStream) 
    delete m_Out; 
}

Obviously you'd need some more checks in useFile and useStdOut to prevent memory leaks.

the_mandrill
  • 29,792
  • 6
  • 64
  • 93
4

Dr. Dobbs published an article that I've used as inspiration for logging. It's worth a read. http://www.drdobbs.com/cpp/201804215

It looks like another article has been published more recently too, but I have not read it. http://www.drdobbs.com/cpp/225700666

Craig Wright
  • 1,575
  • 1
  • 11
  • 19
2

A variation to the_mandrill solution, for that I thought a Strategy pattern would fit this problem better, conceptually.
We can change the log strategy at any time just by calling context->SetLogger.
We can also use different files for the file logger.

class Logger
{
protected:
    ostream* m_os;
public:
    void Log(const string& _s)
    {
        (*m_os) << _s;
        m_os->flush();
    }
};

class FileLogger : public Logger
{
    string m_filename;
public:
    explicit FileLogger(const string& _s)
    : m_filename(_s)
    {
        m_os = new ofstream(m_filename.c_str());
    }
    ~FileLogger()
    {
        if (m_os)
        {
            ofstream* of = static_cast<ofstream*>(m_os);
            of->close();
            delete m_os;
        }
    }
};

class StdOutLogger : public Logger
{
public:
    StdOutLogger()
    {
        m_os = &std::cout;    
    }
};

class Context
{
    Logger* m_logger;
public:
    explicit Context(Logger* _l)  {m_logger = _l;}
    void SetLogger(Logger* _l)    {m_logger = _l;}
    void Log(const string& _s)
    {
        if (m_logger)
        {
            m_logger->Log(_s);
        }
    }
};

int main()
{
    string filename("log.txt");

    Logger*  fileLogger   = new FileLogger(filename);
    Logger*  stdOutLogger = new StdOutLogger();
    Context* context      = new Context(fileLogger);

    context->Log("this log out to file\n");
    context->SetLogger(stdOutLogger);
    context->Log("this log out to standard output\n");
}
rturrado
  • 7,699
  • 6
  • 42
  • 62