Implementing a no-op std::ostream
Asked Answered
C

5

33

I'm looking at making a logging class which has members like Info, Error etc that can configurably output to console, file, or to nowhere.

For efficiency, I would like to avoid the overhead of formatting messages that are going to be thrown away (ie info messages when not running in a verbose mode). If I implement a custom std::streambuf that outputs to nowhere, I imagine that the std::ostream layer will still do all the formatting. Can anyone suggest a way to have a truly "null" std::ostream that avoids doing any work at all on the parameters passed to it with <<?

Calceiform answered 17/4, 2009 at 12:55 Comment(2)
i would not worry. just use a null stream like shown by neil. the class does not need any better performance, because obviously if you don't have a null target, formatting has to be done, so it's obviously not critical. just my 2 centsPerjure
hmm, but it looks like it is intended as a "debug output" thingy? one way i've seen is like this: out() << a << b...; and out() returns struct f { }; with out being template<typename T> f const& operator<<(f const& f_, T const) { return f_; }, and then make out return different structs depending on the log level. or make different out functions or whatever.Perjure
S
4

To prevent the operator<<() invocations from doing formatting, you should know the streamtype at compile-time. This can be done either with macros or with templates.

My template solution follows.

class NullStream {
public:
    void setFile() { /* no-op */ }
    template<typename TPrintable>
    NullStream& operator<<(TPrintable const&)
    { return *this; } /* no-op */
}

template<class TErrorStream> // add TInfoStream etc
class Logger {
public:
    TErrorStream& errorStream() {
        return m_errorStream;
    }

private:
    TErrorStream m_errorStream;
};

//usage
int main() {
    Logger<std::ofstream> normal_logger; // does real output
    normal_logger.errorStream().open("out.txt");
    normal_logger.errorStream() << "My age is " << 19;

    Logger<NullStream> null_logger; // does zero output with zero overhead
    null_logger.errorStream().open("out.txt"); // no-op
    null_logger.errorStream() << "My age is " << 19; // no-op
}

Since you have to do this at compile-time, it is of course quite inflexible.

For example, you cannot decide the logging level at runtime from a configuration file.

Santiagosantillan answered 17/4, 2009 at 13:11 Comment(4)
+1: simple, clean, does the job well. Note that e.g. "NullStream s; s << expensive_function();" will most likely still evaluate expensive_function(), especially if it lives in another module.Drais
I'll also mention that, unlike Neil's onullstream, your NullStream can't be passed to a function expecting an ostream& or ostream* argument.Drais
expensive_function() will definitely be evaluated no matter where it lives. There is no way to pervent that barring macros and conditional compilation :) ... as to Neil's onullstream, I do not believe it fulfils the "zero formatting overhead" requirement :)Santiagosantillan
That's true, Neil's has a different tradeoff. I suppose your class could offer a conversion function to ostream& that returns an instance of Neil's class to get the best of both worlds ;)Drais
S
16

A swift google came up with this example which may be of use. I offer no guarantees, except that it compiles and runs :-)

#include <streambuf>
#include <ostream>

template <class cT, class traits = std::char_traits<cT> >
class basic_nullbuf: public std::basic_streambuf<cT, traits> {
    typename traits::int_type overflow(typename traits::int_type c)
    {
        return traits::not_eof(c); // indicate success
    }
};

template <class cT, class traits = std::char_traits<cT> >
class basic_onullstream: public std::basic_ostream<cT, traits> {
    public:
        basic_onullstream():
        std::basic_ios<cT, traits>(&m_sbuf),
        std::basic_ostream<cT, traits>(&m_sbuf)
        {
            init(&m_sbuf);
        }

    private:
        basic_nullbuf<cT, traits> m_sbuf;
};

typedef basic_onullstream<char> onullstream;
typedef basic_onullstream<wchar_t> wonullstream;

int main() {
    onullstream os;
    os << 666;
}
Songwriter answered 17/4, 2009 at 13:12 Comment(3)
+1. Yes, I guess that deriving from std::basic_ostream<> is necessary if you want to pass a do-nothing stream into a function expecting an ostream& or ostream* parameter -- Iraimbilanja's trick won't work there.Drais
it could only be accepted as ostream *, not ostream &, corrrect?Polynesian
@Polynesian std::ostream is an alias for std::basic_ostream<char, std::char_traits<char>>. This class publicly inherits from thatHammack
C
9

all, thanks for sharing the code, I just do a test, then Neil's method will still do the string formating, for example:

#include <streambuf>
#include <ostream>
#include <iostream>
using namespace std;


template <class cT, class traits = std::char_traits<cT> >
class basic_nullbuf: public std::basic_streambuf<cT, traits> {
    typename traits::int_type overflow(typename traits::int_type c)
    {
        return traits::not_eof(c); // indicate success
    }
};

template <class cT, class traits = std::char_traits<cT> >
class basic_onullstream: public std::basic_ostream<cT, traits> {
    public:
        basic_onullstream():
        std::basic_ios<cT, traits>(&m_sbuf),
        std::basic_ostream<cT, traits>(&m_sbuf)
        {
            init(&m_sbuf);
        }

    private:
        basic_nullbuf<cT, traits> m_sbuf;
};

typedef basic_onullstream<char> onullstream;
typedef basic_onullstream<wchar_t> wonullstream;

class MyClass
{
    int a;
    friend ostream& operator<< (ostream&, MyClass const&);
};

ostream& operator<<(ostream& out,MyClass const& b)
{
    std::cout<<"call format function!!";
    out << b.a;
    return out;
}

int main() {
    onullstream os;
    MyClass obj;
    os<<obj;
}

Running this program, you will find that "ostream& operator<<(ostream& out,MyClass const& b)" will be called. So, doing format on the obj will still be called. So, we still can't avoid the overhead of formatting messages.

Chalcanthite answered 16/7, 2010 at 2:3 Comment(1)
AFAIK the optimiser can remove code which is deems has no side effects. Since your call to std::cout << "call format function!!" does have side effects, then the optimiser won't remove this. However, without the call, it is possible this will be removedHierarchy
S
4

To prevent the operator<<() invocations from doing formatting, you should know the streamtype at compile-time. This can be done either with macros or with templates.

My template solution follows.

class NullStream {
public:
    void setFile() { /* no-op */ }
    template<typename TPrintable>
    NullStream& operator<<(TPrintable const&)
    { return *this; } /* no-op */
}

template<class TErrorStream> // add TInfoStream etc
class Logger {
public:
    TErrorStream& errorStream() {
        return m_errorStream;
    }

private:
    TErrorStream m_errorStream;
};

//usage
int main() {
    Logger<std::ofstream> normal_logger; // does real output
    normal_logger.errorStream().open("out.txt");
    normal_logger.errorStream() << "My age is " << 19;

    Logger<NullStream> null_logger; // does zero output with zero overhead
    null_logger.errorStream().open("out.txt"); // no-op
    null_logger.errorStream() << "My age is " << 19; // no-op
}

Since you have to do this at compile-time, it is of course quite inflexible.

For example, you cannot decide the logging level at runtime from a configuration file.

Santiagosantillan answered 17/4, 2009 at 13:11 Comment(4)
+1: simple, clean, does the job well. Note that e.g. "NullStream s; s << expensive_function();" will most likely still evaluate expensive_function(), especially if it lives in another module.Drais
I'll also mention that, unlike Neil's onullstream, your NullStream can't be passed to a function expecting an ostream& or ostream* argument.Drais
expensive_function() will definitely be evaluated no matter where it lives. There is no way to pervent that barring macros and conditional compilation :) ... as to Neil's onullstream, I do not believe it fulfils the "zero formatting overhead" requirement :)Santiagosantillan
That's true, Neil's has a different tradeoff. I suppose your class could offer a conversion function to ostream& that returns an instance of Neil's class to get the best of both worlds ;)Drais
G
0

Probably you'll need more than just text formatting and message filtering. What about multithreading?

I would implement the filtering and multithreading synchronization as the responsibility of a separate class.

However, logging is a not-so-simple problem, and I would try to use existing logging solutions, instead of developing a new one.

Gravimeter answered 17/4, 2009 at 13:2 Comment(0)
V
0

Why not using existing logging solutions used by millions of users? log4j, log4net, log4cxx.., to name just a few..

Vestryman answered 17/4, 2009 at 13:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.