1

I'm working on a logger class in C++ that has the following syntax:

Logger log("mylog.txt");
log << "a string" << " " << 1 << " " << 6.2 << "\n";

And it prints: a string 1 6.2

This is what my class looks like:

class Logger 
{
private:
    unique_ptr<ofstream> m_pOutStream;

public: 
    Logger(std::string sFile) : m_pOutStream(new ofstream(sFile, std::ios::app))
    {}

    template<typename T>
    Logger& operator<< (const T& data)
    {
        *m_pOutStream << data;
        return *this;
    }
};

It works fine, but I would also like to add a prefix to every line (e.g. a timestamp). So when I write:

Logger log("mylog.txt");
log << "a string" << " " << 1 << " " << 6.2 << "\n";

I want something like this to be displayed:

11:59:12 a string 1 6.2

I have thought of a couple of solutions:

1.Keep every input stored in a list/stream and use an extra function to print and then clear the list/stream:

Logger log("mylog.txt");
log << "a string" << " " << 1 << " " << 6.2 << "\n";
log.logd(); // <- this prints and then clears the internal stream/list.

2.Keep every input stored in a list/stream and print everything after a "new line" character is detected. And clear the internal stream/list after that.

Both of these solutions are nice but I'd prefer to use them only as a last resort.

Is there any other/better way to achieve what I want?

conectionist
  • 2,694
  • 6
  • 28
  • 50
  • An aside but shouldn't your overloaded `operator<<` return a `std::ostream&` rather than reference to `Logger`? – sjrowlinson May 27 '16 at 19:25
  • 1
    Use a flag that indicates whether the last character output was a newline. When you get called and the flag is set, output the prefix first. The flag should default to `true` so that you print the prefix before the first line of output as well. – Barmar May 27 '16 at 19:26
  • In my experience, it's good to separate the "line" concept from the "log entry" concept and timestamp entries. – molbdnilo May 27 '16 at 19:27
  • @molbdnilo Having different formats for different lines in log files makes writing tools that process them more diifficult. – Barmar May 27 '16 at 19:28
  • @ArchbishopOfBanterbury yes, if you want your object to be "printable" (i.e. passed as a parameter to cout). This is not the case. Besides, I don't have a ostream object to return a reference to. – conectionist May 27 '16 at 19:30
  • @Barmar What you're describing is the second solution that I mentioned. Like I said, I would rather not use any of those solutions if there is a better approach. – conectionist May 27 '16 at 19:31
  • @Barmar Good point. I guess I'm biased from always needing to "hand-read" logs. A specific entry separator might be what I really need. (If only I could convince my colleagues...) – molbdnilo May 27 '16 at 19:32
  • 1
    @conectionist My suggestion doesn't require saving any of the output. It just requires one variable, `bool last_char_was_newline;` – Barmar May 27 '16 at 19:33
  • @Barmar Won't the input be lost if I don't save it? Can you please elaborate? – conectionist May 27 '16 at 19:36
  • Why would it be lost? You're going to put it in the log file. – Barmar May 27 '16 at 19:42
  • 1
    Related: [Overload handling of std::endl?](http://stackoverflow.com/questions/2212776/) and [C++ cout with prefix](http://stackoverflow.com/questions/27336335/). – Remy Lebeau May 27 '16 at 20:16

3 Answers3

1

You need to introduce an additional wrapper class for Logger that knows whether the line is starting or being appended.

class Appender
{
    Appender(Logger& logger) : os_(os) { }
    Appender& operator <<(const T& x) { os_ << x; return *this; }
};

class Logger
{
    Appender operator <<(const T& x) { os_ << timestamp() << x; return Appender(os_); }
};
Inverse
  • 4,408
  • 2
  • 26
  • 35
  • Isn't there an issue with returning `Appender&` from the `Logger::operator<<` ? – kmdreko May 27 '16 at 20:07
  • Proxy class looks like an elegant solution, however note that the OP wanted to print timestamp on *every line*, so calling `logger << "something"; logger << "\n";` would not work with your code as desired. You found a solution for a bit different problem. – Rames May 27 '16 at 20:22
1

The actual code will be more complicated, but try implementing the following logic.

Add a member_variable bool last_char_was_newline, and use it in the code like this:

template<typename T>
Logger& operator<< (const T& data)
{
    if (last_char_was_newline) {
        *m_pOutStream << current_time_string();
        last_char_was_newline = false;
    }
    *m_pOutStream << data;
    if (last_char(data) == '\n') {
        last_char_was_newline = true;
    }
    return *this;
}

To be more general, you should scan data for embedded newlines, and put the time after each of them as well.

The above pseudo-code is glossing over the tricky part. Since data can be any type, last_char(data) (and the more general scanning of the output for embedded newlines) is non-trivial. A general way to implement it might be to write data to a std::stringstream. Then you can scan this string for newlines, and finally output the string to *m_pOutStream.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Well it will be much more complicated... Your code is basically useless. You need to handle `std::string`, `char` and `std::endl` overloads at least, providing good type deduction. – Rames May 27 '16 at 20:24
  • I understand that. Like I said, I was trying to show the general logic, not the exact code. I could have written pseudo-code, but I decided to show it using C++ syntax. – Barmar May 27 '16 at 20:27
  • yes, the idea seems good, but it is not that trivial to implement in generic way. – Rames May 27 '16 at 20:37
  • I've added an additional idea about how to do it in a generic way using `std::stringstram`. – Barmar May 27 '16 at 20:42
1

Derive a class from std::stringbuf, say LoggerStringBuf, and give it a reference to your output std::ofstream in its constructor. Override the virtual std::stringbuf::sync() method to retrieve a std::string from the base std::stringbuf::str() method and prefix it with a timestamp when writing it to the std::ofstream. This way you generate a new timestamp every time your LoggerStringBuf object is flushed to the std::ofstream for any reason, whether explicitly by std::endl or std::flush, or implicitly by its destructor.

Then have your Logger class derive from std::ostream and initialize it with a LoggerStringBuf object. Then you can stream input values to your Logger and they will be cached in your LoggerStringBuf object until flushed to the std::ofstream. At which time you can prepend timestamps as needed.

For example:

class LoggerStringBuf : public std::stringbuf
{
private:
    std::ostream &m_OutStream;

protected:
    virtual int sync()
    {
        int ret = std::stringbuf::sync();

        std::string s = str();
        str("");

        // note sure if the string includes non-flushing
        // line breaks.  If needed, you can use std::getline()
        // to break up the string into multiple lines and 
        // write a timestamp for each line...
        //
        m_OutStream << "[timestamp] " << s << std::endl;

        return ret;
    };

public:
    LoggerStringBuf(std::ostream &OutStream)
        : std::stringbuf(std::ios_base::out), m_OutStream(OutStream)
    {
    }

    ~LoggerStringBuf()
    {
        sync();
    }
};

class Logger : public std::ostream
{
private:
    std::ofstream m_OutStream;
    LoggerStringBuf m_Buf;

public: 
    Logger(const std::string &sFile)
        : std::ostream(0), m_OutStream(sFile, std::ios::app), m_Buf(m_OutStream)
    {
        init(&m_Buf);
    }

    template<typename T>
    std::ostream& operator<< (const T& data)
    {
        return static_cast<std::ostream&>(*this) << data;
    }
};
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • What's the purpose of those two lines in the implementation of LoggerStringBuf::sync() ? `std::string s = str(); str("");` – dgellow May 28 '19 at 12:19
  • 1
    @dgellow [read the documentation](https://en.cppreference.com/w/cpp/io/basic_stringbuf/str) on cppreference.com. `sync()` is called to write cached data to the target output. The code is retrieving the cached data from the base class and then resetting the cache in the base class, so the same data does not get logged if `sync()` is called again later. – Remy Lebeau May 28 '19 at 16:18