How to create functions like std::cout?
Asked Answered
N

6

4

I'm creating my own logging utility for my project, I want to create a function like iostream's std::cout, to log to a file and print to the console as well.

Here's what i want:

enum
{
    debug, error, warning, info
};

LOG(level) << "test"; // level - from the above enum

The result should be like this:

int iPlayerID = 1337;
LOG(info) << "Player " << iPlayerID << "Connected";

[Thu Jan 29 18:32:11 2015] [info] Player 1337 Connected

Nomination answered 29/1, 2015 at 14:53 Comment(6)
If on Linux or POSIX consider syslog(3). Framework libraries like POCO or Qt provide logging facilities.Accusation
Both POCO & Qt are cross-platformAccusation
Why do you need the enum at all? Different files to log to?Kenleigh
No, The output will be like this: [Thu Jan 29 18:32:11 2015] [<loglevel>] MessageNomination
Wait, what happens if you do LOG(level) << "hello " << "world"? Do you want [Thu Jan 29 18:32:11 2015] [1] hello [Thu Jan 29 18:32:11 2015] [1] world? That seems like a bad idea.Kenleigh
@Yakk No, i don't like that! Result must be [Thu Jan 29 18:32:11 2015] [1] hello worldNomination
R
8

std::cout is not a function, it's an object of type std::ostream which overloads operator<<.

A quick sketch of how you could do it:

enum Level {
    debug, error, warning, info
};

struct Logger {
    std::ostream* stream;  // set this in a constructor to point
                           // either to a file or console stream
    Level debug_level;
public:
    Logger& operator<<(const std::string& msg)
    {
        *stream << msg; // also print the level etc.
        return *this;
    }

    friend Logger& log(Logger& logger, Level n);
    {
        logger.debug_level = n;
        return logger;
    }
};

Ant then use it like

Logger l;
log(l, debug) << "test";
Resplendent answered 29/1, 2015 at 15:6 Comment(2)
It's the logger class. I deviated a little from your LOG(level) syntax you wanted, because I think you'll regret it latter when you'll want to have several loggers in the same program. So I added the logger as a parameter (I also don't like singletons :) ).Resplendent
Need to fix that friend. It's broken right now (returning *this and all).Negativism
G
5

The trick is for your LOG(level) to return a special type which contains a pointer to an std::ostream, and defines the << operator. Something like:

class LogStream
{
    std::ostream* myDest;
public:
    LogStream( std::ostream* dest ) : myDest( dest ) {}

    template <typename T>
    LogStream& operator<<( T const& obj )
    {
        if ( myDest != nullptr ) {
            *myDest << obj;
        }
        return *this;
    }
};

The LOG(level) macro creats an instance of one, something like:

#define LOG(level) LogStream( getLogStream( level, __FILE__, __LINE__ ) )

Of course, the getLogStream may insert any information it wants (like a timestamp) at the moment it is called.

You might want to add a flush in the destructor of LogStream.

Global answered 29/1, 2015 at 15:8 Comment(0)
G
2

I will not enter coding details here, but I will provide you some quick guidelines :

  1. Create a singleton object pool (for loggers is ok to create a singleton) or a namespace or a that returns a certain log class according to the enum :

    Logger& SingletonLoggersManager::GetLoggerForLevel(eLogLevel);

  2. Override the "<<" operator for your class in order to allow outputting accoridng to your needs in your Logger class

https://msdn.microsoft.com/en-us/library/1z2f6c2k.aspx

  1. Define a macro in order to be able to make a quick call inside your code :

    #define LOG(x) SingletonLogger::GetLoggerForLevel(eLogLoevel);

Now when you use inside your code

 Log(debug) << "test" 

It will expand to :

 (SingletonLogger::GetLoogerForLevel(debug)) << "test";
Gyroscope answered 29/1, 2015 at 15:7 Comment(0)
K
2

There are two problems I see above. The first is forking your message (to both a file, and the console). The second is wrapping what is written with some extra stuff.

meta_stream handles the operator<< overloading. It uses CRTP to statically dispatch to its child type:

template<class D, class substream>
struct meta_stream {
  D& self() { return *static_cast<D*>(this); } // cast myself to D
  // forwarders of operator<<
  template<class X>
  friend D& operator<<( meta_stream<D>& d, X const& x ) {
    d.self().write_to(x);
    return d.self();
  }
  friend D& operator<<(
    meta_stream<D>& d,
    substream&(*mod_func)(substream&)
  ) {
    d.self().write_to(mod_func);
    return d.self();
  }
};

I had to override << twice because of how std::endl and other modifiers work -- they are the name of an overloaded function.

This solves the problem of outputing the same string to two different ostreams:

template<class substream>
struct double_ostream:
  meta_stream<double_ostream<substream>,substream>
{
  substream* a = nullptr;
  substream* b = nullptr;
  template<class X>
  void write_to( X&&x ) {
    if (d.a) (*d.a) << x;
    if (d.b) (*d.b) << std::forward<X>(x);
  }
  double_ostream( std::basic_ostream<CharT>* a_, std::basic_ostream<CharT>* b_ ):
    a(a_), b(b_)
  {}
  double_ostream(double_ostream const&)=default;
  double_ostream()=default;
  double_ostream& operator=(double_ostream const&)=default;
};

note the use of CRTP via meta_stream. I just have to implement write_to.

First, write your 4 loggers to this array:

enum loglevel {
  debug, error, warning, info
};
double_stream<std::ostream> loggers[4];

giving each a pointer to a std::cout and a pointer to a (stored elsewhere) stream wrapping a file you want to save the log to. You can pass nullptr if you don't want that level to be logged to that output stream (say, in release, skip debug logs), and you can log stuff to different log file (debug to one file, info to another).

double_stream<std::ostream> log( loglevel l ) {
  double_stream<std::ostream> retval = loggers[l];
  std::string message;
  // insert code to generate the current date here in message
  // insert code to print out the log level here into message
  retval << message;
  return retval;
}

now log(debug) << "hello " << "world\n" will write your message for you.

You can do more fancy stuff if you don't want to write the newline at the end of the log message, but I doubt it is worth it. Just write the newline.

If you really want that feature:

template<class substream>
struct write_after_ostream:
  meta_stream<write_after_ostream<substream>,substream>
{
  substream* os = nullptr;
  template<class X>
  void write_to( X&&x ) {
    if (os) *os << std::forward<X>(x);
  }
  ~write_after_ostream() {
    write_to(message);
  }
  write_after_ostream( substream* s, std::string m ):
    os(s), message(m)
  {}
  std::string message;
}

write_after_ostream<double_stream<std::ostream>> log( loglevel l ) {
  // note & -- store a reference to it, as we will be using a pointer later:
  double_stream<std::ostream>& retval = loggers[l];
  std::string pre_message;
  // insert code to generate the current date here in pre_message
  // insert code to print out the log level here into pre_message
  retval << pre_message;
  return {&retval, "\n"};
}

but I don't think it is worth it.

Kenleigh answered 29/1, 2015 at 15:11 Comment(0)
A
0

You could define an enum like

enum loglevel_en {
    log_none, log_debug, log_info, log_waring, log_error };

then have a global variable:

enum loglevel_en my_log_level;

and provide some way to set it (e.g. from program arguments).

At last, define a macro (in a global header)

#define MY_LOG(Lev,Thing) do { if (my_log_level >= Lev) \
    std::cout << __FILE__ << ":" << __LINE__ \
              << " " << Thing << std::endl; } while(0)

and use it like

 MY_LOG(log_info, "x=" << x)

Feel free to improve MY_LOG macro to output more things (date, etc...)

You could define instead

#define LOG(Lev) if (my_log_level >= Lev) std::cout 

but that does not play nice with code like

if (x>34)
  LOG(log_info) << "strange x=" << x;
else return;

So I strongly suggest something like MY_LOG

Accusation answered 29/1, 2015 at 15:2 Comment(0)
W
0

There's only one decent solution, and it's not simple, but it gets LOG(log_info) << "line 1\nline 2" << std::endl; correct.

You must implement a custòm std::ostreambuf. It's the only class that can reformat output after all the usual operator<< functions have been applied.

In particular, your stream buffer method overflow is called when you've got a bunch of characters to work on. You can now add your loglevel filtering, and reliably check for newlines in the formatted character stream.

Wowser answered 29/1, 2015 at 20:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.