0

I have a C++ class which logs messages to a std::ofstream. In the class definition is this code:

#ifdef NDEBUG
#define FOO_LOG(msg) /* calls to log messages are no-op in release mode */
#else
std::ofstream log_stream;
#define FOO_LOG(msg) if (log_stream.is_open()) { log_stream << msg << std::endl; }
#endif

The constructor for that class has several things it checks, and in certain situations will open the log file similar to this:

#ifndef NDEBUG
log_stream.open("output.log");
#endif

Then in the code, it calls the C macro like this:

FOO_LOG("stuff=" << stuff << " in loop counter #" << xyz)

It is convenient that you can pass multiple params to the FOO_LOG() macro. There are obvious limitations, like when logging messages from multiple threads, but this is only used for simple logging in debug builds.

What I want to know is whether there is a different/better way to deal with the msg parameter in C++? Is there a simpler/cleaner way to implement something similar to FOO_LOG() in C++?

Stéphane
  • 19,459
  • 24
  • 95
  • 136
  • Yes, it is. Something like `FOO_LOG(logLevel) << "stuff=" << stuff << " in loop counter #" << xyz;`. – 273K Dec 13 '19 at 23:34
  • C++11 and later, you can use a templated function with a parameter pack or a variadic macro – Peter Dec 13 '19 at 23:40
  • @S.M. Make your comment into an answer so people can comment on it. I fail to understand how what you wrote would possibly compile. – Stéphane Dec 13 '19 at 23:40
  • @Stéphane What does "*something similar*" mean? Do you want to maintain the syntax at the call site? I guess implementing the whole thing as a function template called as `FOO_LOG("stuff=", stuff, " in loop counter #", xyz)` or in the way S.M. suggests would make it possible to drop the macros completely and make it somewhat cleaner. – walnut Dec 14 '19 at 01:12

1 Answers1

0

Instead of having #ifdef NDEBUG everywhere in your code you can have a #ifdef NDEBUG at the start of the header to define other useful macros.

For me FOO_LOG("stuff=" << stuff << " in loop counter #" << xyz); feels a bit unnatural. I would have to keep reffering back to the macro definition to see how it's implemented. Instead you can define a macro to behave as an std::ostream and use it as you would any other stream. That way you can do stuff like LOG_STREAM << "stuff=" << stuff << " in loop counter #" << xyz << std::endl;.

For this answer I had some inspiration from this question.

#include <fstream>

#define NDEBUG

//An ostream class that does nothing
class DummyStream : public std::ostream {
public:
    int overflow(int c) { return c; }
} dummyStream;

//Here we let NDEBUG define other macros.
#ifdef NDEBUG
    std::ofstream logStream;
    std::ostream& oLogStream(*(std::ostream*)&logStream);
    #define LOG_STREAM (logStream.is_open() ? oLogStream : dummyStream)
    #define OPEN_LOG(log) (logStream.is_open() ? logStream.close(), logStream.open(log, std::ofstream::out | std::ofstream::app) : \
                                                 logStream.open(log, std::ofstream::out | std::ofstream::app))
    #define CLOSE_LOG() (logStream.close())
#else
    #define LOG_STREAM (dummyStream)
    #define OPEN_LOG(log) //no-op
    #define CLOSE_LOG()   //no-op
#endif // NDEBUG

int main(int argc, char* argv[]) {

    //will only log if NDEBUG is defined.
    OPEN_LOG("log.txt");
    std::string stuff("stuff to log");
    for(int xyz = 0; xyz < 4; xyz++) {
        LOG_STREAM << "stuff=" << stuff << " in loop counter #" << xyz << std::endl;
    }
    CLOSE_LOG();

    //Log is not open so it will not log anything.
    stuff = "stuff to not log";
    for(int xyz = 0; xyz < 4; xyz++) {
        LOG_STREAM << "stuff=" << stuff << " in loop counter #" << xyz << std::endl;
    }

    return 0;
}

Benifits of this?

  • LOG_STREAM is treated more intuitively like any other stream.
  • OPEN_LOG(log) will close already open logs before opening a new one and when NDEBUG is not define it will do nothing.
  • CLOSE_LOG() will close a log and when NDEBUG is not define it will do nothing.
  • Don't need to type #ifdef NDEBUG ... #endif everywhere.
  • dummyStream can be replaced with something like std::cout or some other stream.

Down side?

You have this DummyStream that's just in your code and a few no-op's are performed when LOG_STREAM is used when NDEBUG is not defined.