11
\$\begingroup\$

There are a plethora of streams and streambuffers available today, none are as fast as a memory streambuf, that you can use for testing. Shoot away!

#ifndef MEMSTREAMBUF_HPP
# define MEMSTREAMBUF_HPP
# pragma once

#include <cassert>

#include <cstring>

#include <array>

#include <iostream>

#include <streambuf>

template <::std::size_t N>
class memstreambuf : public ::std::streambuf
{
  ::std::array<char, N> buf_;

public:
  memstreambuf()
  {
    setbuf(buf_.data(), buf_.size());
  }

  ::std::streambuf* setbuf(char_type* const s,
    ::std::streamsize const n) final
  {
    auto const begin(s);
    auto const end(s + n);

    setg(begin, begin, end);
    setp(begin, end);

    return this;
  }

  pos_type seekpos(pos_type const pos,
    ::std::ios_base::openmode const which = ::std::ios_base::in |
    ::std::ios_base::out) final
  {
    switch (which)
    {
      case ::std::ios_base::in:
        if (pos < egptr() - eback())
        {
          setg(eback(), eback() + pos, egptr());

          return pos;
        }
        else
        {
          break;
        }

      case ::std::ios_base::out:
        if (pos < epptr() - pbase())
        {
          setp(pbase(), epptr());
          pbump(pos);

          return pos;
        }
        else
        {
          break;
        }

      default:
        assert(0);
    }

    return pos_type(off_type(-1));
  }

  ::std::streamsize xsgetn(char_type* const s,
    ::std::streamsize const count) final
  {
    auto const size(::std::min(egptr() - gptr(), count));

    ::std::memcpy(s, gptr(), size);

    gbump(size);

    return egptr() == gptr() ? traits_type::eof() : size;
  }

  ::std::streamsize xsputn(char_type const* s,
    ::std::streamsize const count) final
  {
    auto const size(::std::min(epptr() - pptr(), count));

    ::std::memcpy(pptr(), s, size);

    pbump(size);

    return epptr() == pptr() ? traits_type::eof() : size;
  }
};

template <::std::size_t N = 1024>
class memstream : public memstreambuf<N>,
  public ::std::istream,
  public ::std::ostream
{
public:
  memstream() : ::std::istream(this),
    ::std::ostream(this)
  {
  }
};

#endif // MEMSTREAMBUF_HPP
\$\endgroup\$

2 Answers 2

8
\$\begingroup\$

Assert

I personally dislike using asserts. The problem for me is that they do different things in production and debug code. I want the same action in both.

  pos_type seekpos(pos_type const pos, ::std::ios_base::openmode const which) final
  {
    switch (which)
    {
      case ::std::ios_base::in:
      case ::std::ios_base::out:
      default:
        assert(0);
    }

    return pos_type(off_type(-1));
  }

So in this code if we get to the default action; the debug version will assert (and stop the application) while production code will return -1.

In my opinion if you get to a point where your code should not then throw an exception that should not be caught. That will cause the application to exit (in a controlled way).

But in this case I think the expected behavior is not to throw but to return -1. So I would make the default action a no-op.

seekpos which

The which in your seekpos() has basically three settings. You only check for two of the three.

    switch (which)
    {
      case ::std::ios_base::in:
      case ::std::ios_base::out:

      // You forgot this case
      case ::std::ios_base::in | ::std::ios_base::out:

      // it means set the position of the input and output stream.
      // since this is the default value of `which` I would expect
      // this to happen most often (and would assert in your code).

      default:
        assert(0);
    }

    return pos_type(off_type(-1));
  }

xsgetn/xsputn eof

I think you return result of these functions can be incorrect. You should only return eof if you did not get/put any values. You return eof if you have filled/emptied all the data.

  ::std::streamsize xsgetn(char_type* const s,
    ::std::streamsize const count) final
  {
    // If there is no data to get then return eof
    if (egptr() == gptr()) {
        traits_type::eof();
    }

    auto const size(::std::min(egptr() - gptr(), count));

    ::std::memcpy(s, gptr(), size);

    gbump(size);

    return size; // return the number of bytes read.
  }

Overall Design

The input and output use the same buffer but are not linked together. If you have not written anything into the buffer I would not expect you to be able to read from the buffer (as there is nothing to read).

When you do write I would only expect you to be able to read only what has written (no more).

int main()
{
    memstream<1024>   buffer;

    if (buffer.write("abcdef", 6))
    {
         std::cout << "Write OK\n";
    }

    char data[100];
    if (buffer.read(data, 10))
    {
        auto c = buffer.gcount();

        std::cout << "Count: " << c << "\n";
        std::cout << std::string(data, data+10) << "\n";
    }
}

Result:

Write OK
Count: 10
abcdef

So I wrote 6 characters. But I managed to read 10 characters. What were the last 4 characters?

Circular Buffer

Once you have linked the two buffers correctly. You could get more from the buffer by making it circular. That means as you get to the end of the write buffer you can circle around and start writing at the beginning again if you have been reading from the buffer and there is space.

Inheriting from the buffer

template <::std::size_t N = 1024>
class memstream :
  public memstreambuf<N>,    // This is unusual
  public ::std::istream,
  public ::std::ostream
{
public:
  memstream() : ::std::istream(this),
    ::std::ostream(this)
  {
  }
};

Normally I have seen this as a member object rather than inheriting from the buffer. Though this technique saves a call to setbuffer. I can live with it.

BUT: if you are going to inherit from it then I would use private inheritance.

template <::std::size_t N = 1024>
class memstream :
  private memstreambuf<N>,    // Note the private
  public ::std::istream,
  public ::std::ostream
{
public:
  memstream() : ::std::istream(this),
    ::std::ostream(this)
  {
  }
};

This is because I don't want my object to behave like a stream and a stream buffer to people who use it. That could be confusing. So hide the buffer properties from external users (by using private). They can always get a reference to the buffer using the method rdbuf().

pragma once and include guards

There is no point in using both:

#ifndef MEMSTREAMBUF_HPP
# define MEMSTREAMBUF_HPP
# pragma once

....

#endif

I would use the header guards because not all compiler support #pragma once.

Note the space between # and the word is non standard. Most compilers may be forgiving about it but not all so I would not do it.

# define MEMSTREAMBUF_HPP
 ^  // extra space.
\$\endgroup\$
2
\$\begingroup\$

If you can use old C++ just try https://en.cppreference.com/w/cpp/io/strstreambuf/strstreambuf but it deprecated now (how to do it after C++11?)

char buf[10];
std::strstreambuf user(buf, 10, buf); // user-provided output buffer
std::ostrstream user_s(buf, 10); // equivalent stream
user_s << 1.23 << std::ends;
std::cout << buf << '\n';
\$\endgroup\$
3
  • \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Mar 17, 2020 at 9:29
  • \$\begingroup\$ this is just a part of full answer if you follow link \$\endgroup\$ Mar 20, 2020 at 21:13
  • \$\begingroup\$ It is still not a review. And answers need to be complete (links are certainly useful to supplement the information, but an answer cannot rely on a link). \$\endgroup\$ Nov 4, 2020 at 16:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.