patterncppMinor
Creating an istream peeker
Viewed 0 times
creatingpeekeristream
Problem
I want an
Example usage:
Where:
yields:
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
#include
class peekbuf : public std::streambuf
{
std::streambuf* sbuf_;
std::vector data_;
char ch;
public:
peekbuf(std::streambuf* s)
: sbuf_(s)
{ }
std::vector const& peek(int N) {
data_.clear();
data_.reserve(N);
for (int i = 0; i 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
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.outyields:
Got 123456789
Peeked: 12345
Solution
This member needs a good comment:
It's only used in one function, and there it is assigned before any other use, so it's easy to overlook that
Or we could use the existing vector to hold that character when we need it.
The constructor should be
We have a pointer data member, but it's safe to share that between instances. I (and
A better solution would be to recognise that
We have a bug, in that if we
Returning a reference to
My improved version copies the unread peeked characters (I'm not convinced it's safe to
There's a bug in
In the main function, we have:
That's more clearly and succinctly written as:
We might consider using a
Overall enhancement - consider making the class generic, so it can work with wide streams, too:
Full rewrite
Client interface is slightly changed, as we now take a reference -
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(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 peek(std::size_t n)
{
auto unread = std::vector(gptr(), egptr());
data_ = std::move(unread);
data_.reserve(n);
for (auto i = data_.size(); i sbumpc();
if (next == traits_type::eof()) {
break;
}
data_.push_back(static_cast(next));
}
if (!data_.empty()) {
setg(data_.data(), data_.data(), data_.data() + data_.size());
}
return n (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(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>
class peekbuf : public std::basic_streambuf
{
using streambuf = std::basic_streambuf;Full rewrite
template
class peekbuf : public streambuf
{
using typename streambuf::char_type;
using typename streambuf::traits_type;
using typename streambuf::int_type;
using string = std::basic_string;
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() (next));
}
if (!data.empty()) {
setg(data.data(), data.data(), data.data() + data.size());
}
return n (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()};.Code Snippets
// we don't own the pointer, so it's okay to copy
peekbuf(const peekbuf& b) = default;
peekbuf& operator=(const peekbuf& b) = default;data_.push_back(static_cast<char>(next));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_;
}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;
}std::cout << "Peeked: ";
for (char c : first5) {
std::cout << c;
}
std::cout << '\n';Context
StackExchange Code Review Q#127531, answer score: 4
Revisions (0)
No revisions yet.