#include <cassert>

#include <algorithm>
#include <iostream>
#include <iterator>
#include <sstream>
#include <string>

    class LineIterator: public std::iterator<std::input_iterator_tag,
                                             std::string const>
    {
    public:
        // Default Constructible
        LineIterator(): stream(nullptr) {}

        explicit LineIterator(std::istream& is): stream(&is) { this->advance(); }

        // Equality Comparable
        friend bool operator==(LineIterator const& left, LineIterator const& right) {
            return left.stream == right.stream
               and left.buffer == right.buffer
               and left.currentLine == right.currentLine;
        }

        friend bool operator!=(LineIterator const& left, LineIterator const& right) {
            return not (left == right);
        }

        // Trivial Iterator (non mutable)
        pointer operator->() const { return &currentLine; }

        reference operator*() const { return currentLine; }

        // Input Iterator
        LineIterator& operator++() {
            this->advance();
            return *this;
        } // operator++

        LineIterator operator++(int) {
            LineIterator tmp(*this);
            ++*this;
            return tmp;
        } // operator++
        
    private:
        void advance() {
            // Advance a valid iterator to fetch the next line from the source stream.
            static LineIterator const SingularValue;

            assert(*this != SingularValue and "Cannot advance singular iterator");
            // Note: in real life, I would use std::getline...
            // ... but it would not showcase the double-buffering model
            // required to solve the OP problem (because of decoding)

            // We use double-buffering, so clear current and swap buffers
            currentLine.clear();
            swap(buffer, currentLine);

            // Check if we found some new line or not
            size_t const nl = currentLine.find('\n');

            // If we found one already, preserve what's after in the buffer
            // as we only want to expose one line worth of material.
            if (nl != std::string::npos) {
            	if (nl == currentLine.size()) { return; } // nothing to preserve

                buffer.assign(currentLine.begin() + nl + 1, currentLine.end());
                currentLine.erase(currentLine.begin() + nl + 1, currentLine.end());
                return;
            }

            // If we did not, then we need to pump more data into the buffer.
            if (not stream) { return; } // Nothing to pump...
            
            static size_t const ReadBufferSize = 256;
            char input[ReadBufferSize];

            while (stream->read(input, ReadBufferSize)) {
                if (this->splitBuffer(input, ReadBufferSize)) { break; }
            }

            // We end up here either if we found a new line or if some read failed.
            // If the stream is still good, we successfully found a new line!
            if (*stream) { return; }

            // Otherwise, the stream is no good any longer (it dried up!)
            // but we may still have read some little things from it.
            this->splitBuffer(input, stream->gcount());

            stream = SingularValue.stream; // stream dried up,
                                           // so reset it to match singular value.
        } // advance

        bool splitBuffer(char const* input, size_t const size) {
            // Split input at the newline character, the first chunk ends
            // up in currentLine, the second chunk in buffer.
            // Returns true if a newline character was found, false otherwise.

            // Check if we finally found a new line
            char const* const newLine = std::find(input, input + size, '\n');

            // If we did not, copy everything into currentLine and signal it.
            if (newLine == input + size) {
                currentLine.append(input, size);
                return false;
            }

            // If we did, copy everything up to it (including it) into currentLine
            // and then bufferize the rest for the next iteration.
            currentLine.append(input, newLine + 1);
            buffer.assign(newLine + 1, input + size);
            return true;
        } // splitBuffer

        std::istream* stream;
        std::string buffer;

        std::string currentLine;
    }; // class LineIterator


int main() {
	std::stringstream ss;
	ss << "Some string\nwith a couple\n\nnew lines\nthrown in";
	
	std::cout << "$";
	std::copy(LineIterator(ss),
	          LineIterator(),
	          std::ostream_iterator<std::string>(std::cout, "$"));
	return 0;
}