46

I've been googling around and I just can't find a simple answer to this. And it should be simple, as the STL generally is.

I want to define MyOStream which inherits publicly from std::ostream. Let's say I want to call foo() each time something is written into my stream.

class MyOStream : public ostream {
public:
  ...
private:
   void foo() { ... }
}

I understand that the public interface of ostream is non-virtual, so how can it be done? I want clients to be able to use both operator<< and write() and put() on MyOStream and have use the extended ability of my class.

Michael
  • 2,826
  • 4
  • 25
  • 18
  • 3
    The STL might be simple, but that's only one part of the C++ standard library. The iostreams library have nothing to do with (what was once) the STL. STL is basically contianers + iterators + algorithms. Iostreams, locales and all that has a completely different origin, and is generally a pain to work with ;) – jalf Apr 21 '09 at 14:21

6 Answers6

47

I was spinning my head around how to do the same thing and i found out it's actually not that hard.

Basically just subclass the ostream and the streambuf objects, and construct the ostream with itself as the buffer. the virtual overflow() from std::streambuf will be called for every character sent to the stream. To fit your example I just made a foo() function and called it.

#include <iostream>

struct Bar :  private std::streambuf , public std::ostream
{
    Bar() : std::ostream(this) {}

private:
    int overflow(int c) override
    {
        foo(c);
        return 0;
    }


    void foo(char c)
    {
        std::cout.put(c);

    }
};

int main()
{
    Bar b;
    b<<"Look a number: "<<std::hex<<29<<std::endl;

    return 0;
}

EDIT: The old code used the wrong initialization order. Although it had no visible side effects, the streambuf object should be initialized before passing it to the ostream object. Since C++ initializes parents left to right, I moved std::streambuf to the left to make the code correct.

EDIT: I changed the code to inherit std::streambuf privately to keep the interface cleaner and keep the class encapsulated.

aSemy
  • 5,485
  • 2
  • 25
  • 51
Ben
  • 843
  • 8
  • 9
  • 4
    Works perfectly! Should be the accepted answer, though it is an old question. – ZXcvbnM Sep 12 '15 at 17:40
  • 1
    I agree with ZXcvbnM, this should be the accepted answer. The accepted answer contains a useful reference but does not really provide a solution. Ben provides a simple working solution. +1. – sgbirch Jun 06 '17 at 07:15
  • Finally I found a solution after looking for it for so long time, this should be the accepted answer like people said. – user0103 Dec 12 '17 at 11:58
  • Many thanks! After such a long time I finally found a solution in this answer. This should be the accepted one. – Ruurd Adema Dec 04 '18 at 11:26
  • 1
    I guess one usually does not want clients to use `Bar` as `std::streambuf`. In this case private inheritance seems like the way to go: `class Bar : private std::streambuf, public std::ostream {};` – Brandlingo Mar 20 '20 at 10:59
  • @MatthäusBrandl You're right. I updated the code. Plus i added the added methods into the private space. It makes the interface much cleaner. Thanks for the pointing that out. – Ben Nov 14 '20 at 02:37
  • 2
    Calling put() for every character that is put into the stream is very slow. – zomega May 19 '22 at 14:15
  • See my method for how to do appropriate buffering instead of put every character – mach6 Jul 01 '23 at 04:47
26

It's not a simple question, unfortunately. The classes you should derive from are the basic_ classes, such as basic_ostream. However, derivation from a stream may not be what you want, you may want to derive from a stream buffer instead, and then use this class to instantiate an existing stream class.

The whole area is complex, but there is an excellent book about it Standard C++ IOStreams and Locales, which I suggest you take a look at before going any further.

Qix - MONICA WAS MISTREATED
  • 14,451
  • 16
  • 82
  • 145
21

Another working hack to achieve a similar effect is to use template and composition

class LoggedStream {
public:
  LoggedStream(ostream& _out):out(_out){}
  template<typename T>
  const LoggedStream& operator<<(const T& v) const {log();out << v;return *this;}
protected:
  virtual void log() = 0;
  ostream& out;
};

class Logger : LoggedStream {
  void log() { std::cerr << "Printing" << std::endl;}
};

int main(int,char**) {LoggedStream(std::cout) << "log" << "Three" << "times";}
Elazar Leibovich
  • 32,750
  • 33
  • 122
  • 169
  • this doesn't support stuff like `hex` or `endl` – shoosh Mar 04 '13 at 15:57
  • Why not? `T` will get `std::hex`'s type. – Elazar Leibovich Mar 04 '13 at 21:16
  • 13
    @ElazarLeibovich: It works for `std::hex` but not for `std::endl` or `std::flush` (and probably some others). This is because it can not resolve T to the appropriate function type. Adding the following to `LoggedStream` resolves the issue: `LoggedStream const& operator<<(std::ostream& (*F)(std::ostream&)) const { F(out); return *this; }` – Martin York Apr 07 '14 at 23:45
  • @LokiAstari do you know how to do it and still support `std::endl`? – aviggiano Jul 27 '17 at 21:39
6

I don't know if this is correct solution, but I inherited from std::ostream this way. It uses a buffer inherited from std::basic_streambuf and gets 64 characters at a time (or less if flushed) and sends them to a generic putChars() method where the actual handling of data is done. It also demonstrates how to give user data.

Live Example

#include <streambuf>
#include <ostream>
#include <iostream>

//#define DEBUG

class MyData
{
    //example data class, not used
};

class MyBuffer : public std::basic_streambuf<char, std::char_traits<char> >
{

public:

    inline MyBuffer(MyData data) :
    data(data)
    {
        setp(buf, buf + BUF_SIZE);
    }

protected:

    // This is called when buffer becomes full. If
    // buffer is not used, then this is called every
    // time when characters are put to stream.
    inline virtual int overflow(int c = Traits::eof())
    {
#ifdef DEBUG
        std::cout << "(over)";
#endif
        // Handle output
        putChars(pbase(), pptr());
        if (c != Traits::eof()) {
            char c2 = c;
            // Handle the one character that didn't fit to buffer
            putChars(&c2, &c2 + 1);
        }
        // This tells that buffer is empty again
        setp(buf, buf + BUF_SIZE);

        return c;
    }

    // This function is called when stream is flushed,
    // for example when std::endl is put to stream.
    inline virtual int sync(void)
    {
        // Handle output
        putChars(pbase(), pptr());
        // This tells that buffer is empty again
        setp(buf, buf + BUF_SIZE);
        return 0;
    }

private:

    // For EOF detection
    typedef std::char_traits<char> Traits;

    // Work in buffer mode. It is also possible to work without buffer.
    static const size_t BUF_SIZE = 64;
    char buf[BUF_SIZE];

    // This is the example userdata
    MyData data;

    // In this function, the characters are parsed.
    inline void putChars(const char* begin, const char* end){
#ifdef DEBUG
        std::cout << "(putChars(" << static_cast<const void*>(begin) <<
            "," << static_cast<const void*>(end) << "))";
#endif
        //just print to stdout for now
        for (const char* c = begin; c < end; c++){
            std::cout << *c;
        }
    }

};

class MyOStream : public std::basic_ostream< char, std::char_traits< char > >
{

public:

    inline MyOStream(MyData data) :
    std::basic_ostream< char, std::char_traits< char > >(&buf),
    buf(data)
    {
    }

private:

    MyBuffer buf;

};

int main(void)
{
    MyData data;
    MyOStream o(data);

    for (int i = 0; i < 8; i++)
        o << "hello world! ";

    o << std::endl;

    return 0;
}
JDW
  • 460
  • 3
  • 11
Henrik Heino
  • 395
  • 4
  • 9
  • This is flawed. You are passing `basic_ostream` a pointer no a not yet constructed buffer, since the base class is initialized before the data member. See [Ben's answer](https://stackoverflow.com/a/19933011/1969455) for more on this. – Brandlingo Mar 20 '20 at 10:50
0

Ben's method works but it is a terrible method in the real-world application. With his method, there is no in-memory buffer at all and you are essentially outputting every single character.

To achieve the goal, you must create two types of classes.

  1. A filebuf class derived from std::streambuf with virtual methods sync(), overflow(),xsputn(),seekoff(),seekpos() overrode. It also needs to have appropriate buffering.

  2. A stream class derived from std::basic_ostream. It should have a private member as your customized filebuf, for example buf, and call std::basic_ios<CharT,Traits>::init like this->init(&buf) in its constructor. You DON'T need to define any other methods because std::basic_ostream will handle it for you. Once finishing the initiation step, this->rdbuf() will return &buf.

The underlying call stack is like the following

  • basic_ostream& operator<<:
    rdbuf()->sputn, which will call rdbuf()->xsputn

  • basic_ostream& put( char_type ch )
    rdbuf()->sputc, which will call rdbuf()->overflow

  • basic_ostream& write( const char_type* s, std::streamsize count)

    rdbuf()->sputn, which will call rdbuf()->xsputn

  • pos_type tellp()
    rdbuf()->pubseekoff, which will call rdbuf()->seekoff

  • basic_ostream& seekp( off_type off, std::ios_base::seekdir dir )
    rdbuf()->pubseekpos, which will call rdbuf()->seekpos

  • basic_ostream& flush()
    rdbuf()->pubsync, which will call rdbuf()->sync

Here is the an example, the full example is here https://github.com/luohancfd/mpistream

In most scenarios, you only need to change the functions to open_file, close_file, seek_pos, tell_pos, and write_data. By tweaking buf_size, you can get a significant performance improvement.

// GPL v3.0
class MPI_filebuf : public std::streambuf {
public:
  using Base = std::streambuf;
  using char_type = typename Base::char_type;
  using int_type = typename Base::int_type;
  using pos_type = typename Base::pos_type;
  using off_type = typename Base::off_type;

private:
  static const std::streamsize buf_size = BUFSIZ;
  char buffer_[buf_size];

  MPI_File fhw;
  bool opened;

  /**
   * @brief Always save one extra space in buffer_
   *        for overflow
   */
  inline void reset_ptr() { setp(buffer_, buffer_ + buf_size - 1); }

protected:
  /**
   * @brief For output streams, this typically results in writing the contents
   * of the put area into the associated sequence, i.e. flushing of the output
   * buffer.
   *
   * @return int Returns ​0​ on success, -1 otherwise. The base class
   * version returns ​0​.
   */
  inline int sync() override {
    int ret = 0;
    if (pbase() < pptr()) {
      const int_type tmp = overflow();
      if (traits_type::eq_int_type(tmp, traits_type::eof())) {
        ret = -1;
      }
    }
    return ret;
  }

  /**
   * @brief Write overflowed chars to file, derived from std::streambuf
   *        It's user's responsibility to maintain correct sequence of
   *        output as we are using shared file pointer
   *
   * @param ch
   * @return int_type Returns unspecified value not equal to Traits::eof() on
   * success, Traits::eof() on failure.
   */
  inline int_type overflow(int_type ch = traits_type::eof()) override {
    // https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/fstream.tcc
    int_type ret = traits_type::eof();
    const bool testeof = traits_type::eq_int_type(ch, ret);

    if (pptr() == nullptr) {
      reset_ptr();
      if (!testeof) {
        ret = sputc(ch);
      }
    } else {
      if (!testeof) {
        *pptr() = traits_type::to_char_type(ch);
        pbump(1);
      }
      if (write(pbase(), pptr() - pbase())) {
        ret = traits_type::not_eof(ch);
      }
      reset_ptr();
    }
    return ret;
  }

  /**
   * @brief Writes \c count characters to the output sequence from the character
   * array whose first element is pointed to by \c s . Overwrite this function
   * to achieve no_buffered I/O
   *
   * @param s
   * @param n
   * @return std::streamsize
   */
  inline std::streamsize xsputn(const char_type *s,
                                std::streamsize n) override {
    std::streamsize bufavail = epptr() - pptr();
    std::streamsize ret = n;

    // fill buffer up to "buf_size"
    std::streamsize nfill = std::min(n, bufavail + 1);
    std::copy(s, s + nfill, pptr());
    pbump(nfill); // if nfill == bufavail+1, pptr() == epptr()

    if (nfill == bufavail + 1) {
      // equiv: bufavail + 1<= n
      if (!write(pbase(), pptr() - pbase())) {
        ret = -1;
      } else {
        reset_ptr();
        s += nfill;
        n -= nfill;

        /*
          repeatedly write every chunk until there is
          less data than buf_size - 1
        */
        while (n >= buf_size - 1) {
          write(s, buf_size);
          s += buf_size;
          n -= buf_size;
        }
        std::copy(s, s + n, pptr());
        pbump(n);
      }
    }
    return ret;
  }

  /**
   * @brief Sets the position indicator of the input and/or output
   *        sequence relative to some other position. It will flush
   *        the internal buffer to the file
   * @note  This function is collective, which means seekp(), tellp()
   *        need to be called by all processors
   *
   * @param off relative position to set the position indicator to.
   * @param dir defines base position to apply the relative offset to.
   *            It can be one of the following constants: beg, cur, end
   * @param which
   * @return pos_type The resulting absolute position as defined by the position
   * indicator.
   */
  inline pos_type
  seekoff(off_type off, std::ios_base::seekdir dir,
          __attribute__((__unused__))
          std::ios_base::openmode which = std::ios_base::out) override {
    int ret = pos_type(off_type(-1));
    if (is_open()) {
      int whence;
      if (dir == std::ios_base::beg)
        whence = MPI_SEEK_SET;
      else if (dir == std::ios_base::cur)
        whence = MPI_SEEK_CUR;
      else
        whence = MPI_SEEK_END;

      sync(); /*!< write data to the file */
      if (off != off_type(0) || whence != SEEK_CUR) {
        if (MPI_File_seek_shared(fhw, off, whence)) {
          // fail to seek
          return ret;
        }
      }
      MPI_Offset tmp;
      MPI_File_get_position_shared(fhw, &tmp);
      ret = pos_type(tmp);
    }
    return ret;
  }

  inline pos_type seekpos(pos_type pos, __attribute__((__unused__))
                                        std::ios_base::openmode which =
                                            std::ios_base::out) override {
    return seekoff(off_type(pos), std::ios_base::beg);
  }

  /**
   * @brief Method doing the real writing. It moves the data in the
   *        internal buffer to the file
   *
   * @param pbeg
   * @param nch
   * @return true  Succeed to write
   * @return false Fail to write
   */
  inline bool write(const char_type *pbeg, std::streamsize nch) {
    return nch == 0 ||
           !MPI_File_write_shared(fhw, pbeg, nch, MPI_CHAR, MPI_STATUS_IGNORE);
  }

public:
  MPI_filebuf() : buffer_{}, opened(false) {
    setp(buffer_, buffer_ + buf_size - 1);
  }
  virtual ~MPI_filebuf() override {
    if (opened)
      close();
  };

  /**
   * @brief return nullptr if fail
   *
   * @param file_name
   * @return MPI_filebuf*
   */
  MPI_filebuf *open(const char file_name[]);
  inline bool is_open() const { return opened; }
  MPI_filebuf *close() {
    sync();
    return MPI_File_close(&fhw) ? nullptr : this;
  }


};



/* ---------------------------------------------------------------------- */

class mpistream : public std::basic_ostream<char> {
public:
  // Types
  using Base = std::basic_ostream<char>;
  using int_type = typename Base::int_type;
  using char_type = typename Base::char_type;
  using pos_type = typename Base::pos_type;
  using off_type = typename Base::off_type;
  using traits_type = typename Base::traits_type;

  // Non-standard types:
  using filebuf_type = MPI_filebuf;
  using ostream_type = Base;

private:
  filebuf_type filebuf;

public:
  mpistream() : ostream_type(), filebuf() { this->init(&filebuf); }
  mpistream(const char file_name[]) : ostream_type(), filebuf() {
    this->init(&filebuf);
    open(file_name);
  }
  mpistream(const mpistream &) = delete;
  mpistream(mpistream &&__rhs)
      : ostream_type(std::move(__rhs)), filebuf(std::move(__rhs.filebuf)) {
    ostream_type::set_rdbuf(&filebuf);
  }
  ~mpistream() {}

  inline void open(const char file_name[]) {
    if (filebuf.open(file_name) == nullptr) {
      this->setstate(std::ios_base::failbit);
    } else {
      this->clear();
    }
  }
  inline bool is_open() const { return filebuf.is_open(); }
  inline void close() {
    if (!filebuf.close()) {
      this->setstate(ios_base::failbit);
    }
  }

};

mach6
  • 316
  • 5
  • 4
-2

Composition, not inheritance. Your class contains, "wraps" an ostream&, and forwards to it (after calling foo()).

tpdi
  • 34,554
  • 11
  • 80
  • 120
  • 1
    Please post some code that illustrates how this would work with the existing << operators. And note foo() is to be called each time such an operator is used. –  Apr 21 '09 at 12:50
  • 9
    Composition is not always the best solution, just as inheritance isn't. ostream has a dozen of overloaded operators implemented for it, you don't expect anyone to really rewrite all the public interface of ostream just to add a small functionality to a class. – Michael Apr 21 '09 at 12:58
  • 3
    There's also the issue that people write their own operator<< methods that assume an ostream. If you're not actually an ostream, those won't get invoked. – Joseph Larson May 08 '18 at 13:02