Class to output to several ostreams (file and console)
Asked Answered
K

2

3

Right, I'm not even sure how to correctly formulate this; I feel it's a bit of an involved question. I'm sure someone can help me out here though.

This is what I want to do:

  1. Have a single class that I can send stuff to, like this.

    icl << "Blah blah blah" << std::endl;
    
  2. I want to be able to .attach() classes that inherit std::basic_ostream to it.

  3. Those classes would then be able to format the output their own way. One might add a timestamp and write to a log, the other might write it to the console, the other might display it in-game.

Anyone care to get me started in the right direction? Here's the idea I pretty much have.

#include <vector>

class OutputMan {
    std::vector<std::basic_ostream&> m_Streams;

public:
    void attach(std::basic_ostream& os) {
        m_Streams.push_back(os);
    }
}

Question #1: What do I need to inherit and override to send

icl << "Blah!" << std::endl;

To every stream in m_Streams?

Question #2: How do I inherit std::basic_ostream and create a class that changes the output to, for example, add a timestamp to the start of it? I also want for this class to output to a file.

Kerikeriann answered 21/4, 2013 at 19:38 Comment(9)
Question #1: Google operator overloading and you'll see plenty of examples.Jugal
I'm well aware how operator overloading works, but it seems a little more complicated here because there are a large amount of data types that could be fed into the OutputMan, like an integer. Surely there's a better way.Kerikeriann
You could use a template with specific template overloads/instantiations(not sure what it's called) for the types that might need special handling.Jugal
Are you also well-aware of the language restrictions on pointers or arrays (in this case a std::vector<> is somewhat both) of references? That restriction being, you can't? You may want to consider that while working on your design.Janeejaneen
Since OutputMan is itself an output stream either inherit from one or supply a custom streambuf that dispatches the output.Wonted
@Janeejaneen No, I wasn't aware. Is there an alternative way?Kerikeriann
@CaptainObvlious Right, I can do that. That just leaves Question #2.Kerikeriann
@JesseBrands in your case if the container owns the streams I would likely use std::unique_ptr<std::ostream> as the container contents, then create the appropriate stream types on the fly based on what is being registered. If the streams are externally "owned" I would use bare pointers and be VERY public about how the registered streams cannot be destroyed while still registered with the manager.Janeejaneen
@Janeejaneen Thanks. OutputMan never destroys streams; whoever attached the stream should detach it before deleting it.Kerikeriann
B
5

I think I'd do things a bit differently. I've probably made this just a bit more elaborate than necessary -- I'm afraid I may have gotten a little carried away with trying to put new C++11 features to good use. Anyway, on with the code:

#include <streambuf>
#include <fstream>
#include <vector>
#include <iostream>
#include <initializer_list>

namespace multi { 
class buf: public std::streambuf {
    std::vector<std::streambuf *> buffers;
public:
    typedef std::char_traits<char> traits_type;
    typedef traits_type::int_type  int_type;

    buf(std::vector<std::ofstream> &buf) {
        for (std::ofstream &os : buf)
            buffers.push_back(os.rdbuf());
    }

    void attach(std::streambuf *b) { buffers.push_back(b); }

    int_type overflow(int_type c) {
        bool eof = false;
        for (std::streambuf *buf : buffers) 
            eof |= (buf -> sputc(c) == traits_type::eof());
        return eof ? traits_type::eof() : c;
    }   
};

class stream : public std::ostream { 
    std::vector<std::ofstream> streams;
    buf outputs;
public:   
    stream(std::initializer_list<std::string> names)
        : streams(names.begin(), names.end()), 
          outputs(streams), 
          std::ostream(&outputs) 
    { }
    void attach(std::ostream &b) {
        outputs.attach(b.rdbuf());
    }
};
}

int main() { 
    multi::stream icl({"c:\\out1.txt", "c:\\out2.txt"});
    icl.attach(std::cout);

    icl << "Blah blah blah" << std::endl;
}

As you can see, this already accepts manipulators (which should work with any manipulator, not just std::endl). If you want to write to multiple files (things that could/can be opened as fstreams) you can specify as many of those names in the constructor as you like (within the limits imposed by your system, of course). For things like std::cout and std::cerr for which you don't necessarily have a file name, you can use attach as you originally intended.

I suppose I should add that I'm not entirely happy with this as-is. It'd take some fairly serious rewriting to do it, but after some thought, I think the "right" way would probably be for multi::stream's ctor to be a variadic template instead, so you'd be able to do something like: multi::stream icl("c:\\out1.txt", std::cout);, and it would sort out how to use each parameter based on its type. I may update this answer to include that capability some time soon.

As far as the second question goes, I've written another answer that covers the basic idea, but is probably a bit overly elaborate, so the part you care about may kind of get lost in the shuffle, so to speak -- it has quite a bit of logic to deal with line lengths that you don't really care about (but does produce each output line with a specified prefix, like you want).

Belt answered 21/4, 2013 at 22:4 Comment(4)
I'll have to accept this as the answer instead; very elaborate and really got me quite where I wanted. Thanks a lot!, PS: If you do at some point update the answer, do let me know. I'm very curious to see how that would work.Kerikeriann
One question though. I cannot use C++11 for this project, so instead I used boost/foreach.hpp for the foreach loops you had. However, I cannot do this: Mage::IO::MultiStream ICL({"Mage3D.log", "Game.log"});, what gives? Isn't std::initializer_list part of C++ before C++11?Kerikeriann
@JesseBrands: Sorry. std::initializer_list is also new with C++11.Belt
Alright, I've adjusted my code for that; same result as yours though. Thanks for the excellent answer.Kerikeriann
F
4

You may need something like this:

class OutputMan {
    std::vector<std::ostream*> m_Streams;

public:
    void attach(std::ostream *os) {
        m_Streams.push_back(os);
    }

    template <typename T>
    OutputMan &operator<<(const T &t) {

        for (int i=0; i<m_Streams.size(); i++)
            *m_Streams[i] << t;

        return *this;
    }
};

int main() {
    ofstream file("test.txt");

    OutputMan x;
    x.attach(&cout);
    x.attach(&cerr);
    x.attach(&file);

    x << "Hello" << 123;
}

For simplicity I used std::ostream*. To accept stuffs by << I overloaded operator<<.

 

Note: If you want OutputMan accepts std::endl as well as other things, read here.

Fungible answered 21/4, 2013 at 19:51 Comment(5)
This is what I wanted. Would it also be possible to attach ofstreams to this for file output? It still kind of leaves Question #2 unanswered but it's a big step forward at least.Kerikeriann
@JesseBrands: Yes, it's possible, see my updated answer again.Fungible
Thank you very much, you have been a great help.Kerikeriann
One more question, if I made a class that derived from std::ostream, would it OutputMan accept that too? I assuming it does.Kerikeriann
Yes, it can. However deriving from STLs is not recommended.Fungible

© 2022 - 2024 — McMap. All rights reserved.