6

I have a base class e.g. "ProcessingThread", that has several derivations. Each derivation has a specific name, e.g. "DerivationOne", "DerivationTwo", ... Now it seems useful to me, to have a formatted output to console which prints something like:

[DerivationOne]: Action X took place!
[DerivationTwo]: Action Y took place!
[DerivationTwo]: Action Z took place!

Simultaneously it should write the stuff each to a derivation specific log file. I thought of a class, that can be called in the standard manner, e.g. "custom_out << "Write stuff" << std::endl;" and uses the single stream to generate two streams that one runs in console with formatting and second to a log file without formatting the name [name] in the front.

Is there a standard way of doing this? Maybe normal logger already support behaviour like this? Maybe I can derive from a std::stream somehow to accomplish this? What is the best (or at least a good) way?

Ben Jackson
  • 90,079
  • 9
  • 98
  • 150
Alex Pander
  • 95
  • 1
  • 9
  • 1
    Is it a requirement to use chaining on operator<< ? That trick is no longer necessary, (or IMHO desirable), now that we have variadic templates.Would it be OK to define custom_out("Write stuff ", 42, ", and I mean it."); instead? Beware that custom_out << "Write stuff" << std::endl; will trigger argument-dependent-lookup (ADL) that will bring namespace std into play. You don't want that. – Jive Dadson Feb 01 '18 at 13:11
  • 1
    Clarifiying... The function would take any number of arguments, but they would be separated by commas, not << symbols. Is that acceptable? – Jive Dadson Feb 01 '18 at 13:20
  • @JiveDadson : I am absolutely free in the selection of implementation. So sure, it would also be okay. It should be easy and straightforward at best, because I'd like to save time and unnecessary complexity by implementing it. I am not sure how to handle dynamic number of arguments by writing something like a logger – Alex Pander Feb 01 '18 at 13:39
  • 1
    I think I'll give it a go. – Jive Dadson Feb 01 '18 at 13:42
  • Is this an example of what you thought of? https://stackoverflow.com/questions/7031116/how-to-create-function-like-printf-variable-argument – Alex Pander Feb 01 '18 at 14:00
  • Me? No, not an example of WITO. I'm all about c++17. – Jive Dadson Feb 01 '18 at 15:11

2 Answers2

2

Here's a starter-kit for the idea I was discussing in the comments. You will need to decide what to do about errors writing to the disk file - return false, throw exception, or whatever. I edited it to return true/false. True means no error. Work in progress.

#include <iostream>
#include <mutex>
#include <string>
#include <fstream>
#include <string_view>
#include <iomanip>

namespace dj {

    inline bool print(std::ostream& out) {
        return !!(out << std::endl);
    }

    template<typename T>
    bool print(std::ostream& out, T&& value)
    {
        return !!(out << std::forward<T>(value) << std::endl);
    }

    template<typename First, typename ... Rest>
    bool print(std::ostream& out, First&& first, Rest&& ... rest)
    {
        return !!(out << std::forward<First>(first)) && print(out, std::forward<Rest>(rest)...);
    }

    inline std::mutex logger_mtx;

    class log_stream {
    public:
        log_stream(std::string_view str, std::ostream& ifile)
            : name(str)
            , file(ifile)
        {
            std::string s{ "[" };
            name = s + name + "] ";
        }

        template <typename... Args>
        bool operator() (Args&&... args) {
            bool OK = print(file, std::forward<Args>(args)...);
            {
                std::lock_guard<std::mutex> lck(logger_mtx);
                print(std::cout, name, std::forward<Args>(args)...);
                if (!OK) {
                    print(std::cout, name, "-- Error writing to log file. --");
                }
            }
            return OK;
        }

    private:
        std::string name;
        std::ostream& file;
    };


}
int main()
{
    std::ofstream outfile("DerivationOne.log.txt");
    dj::log_stream log("DerivationOne", outfile);

    std::ofstream outfile2; // ERROR. File not opened
    dj::log_stream log2("DerivationTwo", outfile2);

    log("Life. See ", 42, ", meaning of.");
    bool OK = 
      log2("log", std::setw(4), 2.01, " That's all, folks. -", 30, '-');
    std::cout << (OK ? "OK" : "So not OK") << std::endl;
}
Jive Dadson
  • 16,680
  • 9
  • 52
  • 65
1

I think this is a great question.

You actually, in my opinion, consider two different things:

  1. Reformatting of output (prefixing) by the custom stream
  2. Redirecting it to more than one stream

I wouldn't do both in one class. I would write classes for each feature.

For the first, I'd use a simple class, pass a std::ostream reference and a string (for the prefix, although it is probably possible to generalize more than just prefix here) to the constructor, then overload operator<<;

For the second, I'd write a class taking probably two iterators (as e.g. std::sort takes) for the list of streams, and again, overload `operator<<´.

For your class doing both, I'd then pass an object of the second class to the constructor of the first one.

Demosthenes
  • 1,515
  • 10
  • 22
  • Is this strategy able to handle multiple concatenating variables like custom_out << "Variables: " << a << b << c << std::cout; ? – Alex Pander Feb 01 '18 at 13:41
  • The multiple stream thing is of course, trivially so. The other one is, too, but implementation isn't quite as straightforward. You could, for instance, look for `std::endl` (or `'\n'`) and then, well, start a new line. – Demosthenes Feb 05 '18 at 10:27
  • Thinking about it I'd probably insist on `std::endl` for triggering a new prefix, then I could even have multi-line output with `'\n'`. Makes implementation a lot easier, too. – Demosthenes Feb 05 '18 at 10:33