12
\$\begingroup\$

I want an istream that you can safely peek arbitrarily many characters from. This works as far as I can tell, but I am unsure if this is really "the right way" to do it since the iostream library is pretty cryptic to me.

#include <streambuf>
#include <vector>

class peekbuf : public std::streambuf
{
    std::streambuf* sbuf_;
    std::vector<char> data_;
    char ch; 
public:
    peekbuf(std::streambuf* s)
    : sbuf_(s)
    { } 

    std::vector<char> const& peek(int N) {
        data_.clear();
        data_.reserve(N);
        for (int i = 0; i < N; ++i) {
            int next = sbuf_->sbumpc();
            if (next == traits_type::eof()) {
                break;
            }
            data_.push_back(next);
        }

        if (!data_.empty()) {
            setg(data_.data(), data_.data(), data_.data() + data_.size());
        }

        return data_;
    }   

protected:
    int_type underflow() override {
        ch = sbuf_->sbumpc();
        setg(&ch, &ch, &ch + 1); 
        return ch; 
    }   
};

Example usage:

#include <iostream>

int main() {
    peekbuf buf{std::cin.rdbuf()};
    std::cin.rdbuf(&buf);

    auto first5 = buf.peek(5);

    std::string s;
    std::cin >> s;
    std::cout << "Got " << s << '\n';

    std::cout << "Peeked: ";
    for (char c : first5) {
        std::cout << c;
    }
    std::cout << '\n';
}

Where:

$ echo 123456789 | ./a.out

yields:

Got 123456789
Peeked: 12345
\$\endgroup\$
3
  • \$\begingroup\$ I don;t have time write now (this weekend). But something feels wrong. When you set the underlying buffer with setg(data_.data(), data_.data(), data_.data() + data_.size()); I would expect you to save the state of the current buffer so when you have finished reading your peeked data you can go back to the original buffer. \$\endgroup\$ Commented May 5, 2016 at 17:33
  • 1
    \$\begingroup\$ You don't need to go back to the original, you've just set the internal buffer to a new valid state. The only downside here is, of course, that regular i/o buffers support "unget" which setg supports by setting the first two arguments nonequal. You may find that this peek class interferes with operations of some parsers, etc. \$\endgroup\$ Commented Nov 6, 2019 at 13:37
  • \$\begingroup\$ @Toby_Speight I thought that the code in the question should not be edited after the question has been answered? \$\endgroup\$
    – JDługosz
    Commented Oct 27, 2021 at 21:47

1 Answer 1

4
\$\begingroup\$

This member needs a good comment:

   char ch;

It's only used in one function, and there it is assigned before any other use, so it's easy to overlook that setg() retains a pointer to it, and to think its scope can be reduced to that one function.

Or we could use the existing vector to hold that character when we need it.


The constructor should be explicit, and g++ -Weffc++ would like it to also initialize the vector. (That's perhaps overzealous, but pacifying it lets us get some more useful warnings from that option.)

We have a pointer data member, but it's safe to share that between instances. I (and -Weffc++) would appreciate an indication that we have actually thought about that:

// we don't own the pointer, so it's okay to copy
peekbuf(const peekbuf& b) = default;
peekbuf& operator=(const peekbuf& b) = default;

A better solution would be to recognise that sbuf_ must not be null, and so work with a reference instead of a pointer.


peek() should probably take a std::size_t argument, rather than int.

int next probably ought to be int_type next - or better, auto next. I'd like to see the narrowing to char be explicit:

        data_.push_back(static_cast<char>(next));

We have a bug, in that if we peek() before we have read everything from the preceding peek(), we lose input. Instead of clearing data_, we need to be a bit more careful. We need to take care that the subsequent peek() might request less than we have saved in the vector, or more.

Returning a reference to data_ could be risky, since it may be modified before the caller uses it (e.g. if we write auto const& first5 = buf.peek(5);).

My improved version copies the unread peeked characters (I'm not convinced it's safe to assign from a subset of a vector's own elements, and we can't use erase() because vector iterators don't have to be pointers):

std::vector<char> peek(std::size_t n)
{
    auto unread = std::vector<char>(gptr(), egptr());
    data_ = std::move(unread);
    data_.reserve(n);
    for (auto i = data_.size();  i < n;  ++i) {
        auto next = sbuf_->sbumpc();
        if (next == traits_type::eof()) {
            break;
        }
        data_.push_back(static_cast<char>(next));
    }

    if (!data_.empty()) {
        setg(data_.data(), data_.data(), data_.data() + data_.size());
    }

    return n < data_.size()
        ? std::vector<CharT>(data_.begin(), data_.begin() + n)
        : data_;
}

There's a bug in underflow(): it fails to pass through EOF properly, as it narrows to char and returns that narrowed char rather than the original value. Easily fixed:

int_type underflow() override
{
    auto result = sbuf_->sbumpc();
    char ch = static_cast<char>(result); // if it's eof, it's never read anyway
    setg(&ch, &ch, &ch + 1);
    return result;
}

In the main function, we have:

std::cout << "Peeked: ";
for (char c : first5) {
    std::cout << c;
}
std::cout << '\n';

That's more clearly and succinctly written as:

std::cout << "Peeked: "
          << std::string(first5.begin(), first5.end())
          << '\n';

We might consider using a std::string instead of vector throughout - it's probably more useful to callers.


Overall enhancement - consider making the class generic, so it can work with wide streams, too:

template<typename CharT, typename Traits= std::char_traits<CharT>>
class peekbuf : public std::basic_streambuf<CharT, Traits>
{
    using streambuf = std::basic_streambuf<CharT, Traits>;

Full rewrite

template<typename streambuf>
class peekbuf : public streambuf
{
    using typename streambuf::char_type;
    using typename streambuf::traits_type;
    using typename streambuf::int_type;
    using string = std::basic_string<char_type, traits_type>;

    static constexpr auto eof = streambuf::traits_type::eof();

    using streambuf::gptr;
    using streambuf::egptr;
    using streambuf::setg;

    streambuf& sbuf;           // underlying input buffer
    string data = {};

public:
    explicit peekbuf(streambuf& s)
        : sbuf{s}
    { }

    string peek(std::size_t n)
    {
        auto unread = string{gptr(), egptr()};
        data = std::move(unread);
        data.reserve(n);
        while (data.size() < n) {
            auto next = sbuf.sbumpc();
            if (next == eof) {
                break;
            }
            data.push_back(static_cast<char_type>(next));
        }

        if (!data.empty()) {
            setg(data.data(), data.data(), data.data() + data.size());
        }

        return n < data.size()
            ? string(data.begin(), data.begin() + n)
            : data;
    }

protected:
    int_type underflow() override
    {
        auto result = sbuf.sbumpc();
        data.clear();
        data.push_back(static_cast<char_type>(result));
        setg(data.data(), data.data(), data.data() + 1);
        return result;
    }
};

Client interface is slightly changed, as we now take a reference - peekbuf buf{*std::cin.rdbuf()};.

\$\endgroup\$
0

Your Answer

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

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