13

Since it is by design that std::ostream can't be moved the question becomes: how can an std::ostream be moved such that it can write to different destinations?

The basic objective is to have a factory function taking a URI and returning something, let's call it, omstream (output movable stream) which can be used like an std::ostream:

omstream stream_factory(std::string const& uri);
void     process(std::ostream& out);

int main(int ac, char* av[]) {
    omstream destination{ stream_factory(ac == 2? av[1]: "example.txt") };
    process(destination);
}

The omstream would be responsible for properly moving the object:

class omstream
    : public std::ostream {
    // suitable members
public:
    omstream(/* suitable constructor arguments */);
    omstream(omstream&& other) // follow recipe of 27.9.1.11 [ofstream.cons] paragraph 4
        : std:ios(std::move(other))
        , std::ostream(std::move(other))
        // move any members {
        this->set_rdbuf(/* get the stream buffer */);
    }
    // other helpful or necessary members
};

The question is really what it takes to implement omstream (or, even a corresponding class template basic_omstream)?

Community
  • 1
  • 1
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Forgive me if I'm wrong, but isn't calling `std::move` on an rvalue reference a bit...redundant? – chris Dec 25 '13 at 17:07
  • like this ? http://www.cplusplus.com/reference/ios/ios/tie/ – user2485710 Dec 25 '13 at 17:07
  • 4
    @chris: you mean `std::move(other)`? Well, the moment an object has a name it isn't an rvalue. In the argument list it basically indicates that an rvalue can be passed to the function but within the function the parameter is an lvalue. – Dietmar Kühl Dec 25 '13 at 17:08
  • @user2485710: `tie()` does something quite different: it registers the corresponding `std::ostream` as an object of interest which needs to be flushed before the stream to which it is tied can used. It doesn't help with creating a stream in a factory method and returning it. – Dietmar Kühl Dec 25 '13 at 17:09
  • @DietmarKühl, My bad, seems I still don't understand them as well as I should. – chris Dec 25 '13 at 17:14
  • 1
    The implicitly private inheritance is intended? (Never seen it written implicitly..) – dyp Dec 25 '13 at 19:53
  • 2
    Is the first sentence a type-o? The linked question says that `ostream` isn't movable. It does *not* say that `ofstream` is not movable. `ofstream` *is* movable. – Howard Hinnant Dec 25 '13 at 19:57
  • @HowardHinnant: it should have been `std::ostream`. I have fixed the typo. – Dietmar Kühl Dec 25 '13 at 22:30
  • @DyP: The private inheritance wasn't intended: the goal is to have `omstream` be an `std::ostream`. I have fixed the typo. – Dietmar Kühl Dec 25 '13 at 22:31

2 Answers2

8

The code posted in Howard's answer is a draft (based on the draft posted in the question). Howard's answer helped resolving a confusing issue with the virtual base class std::ios: the base class needs to be default constructed when moving a derived stream as the std::ios portion of a stream will explicitly be moved by the std::ostream move constructor using std::ios::move(). This answer merely fills in the missing bits.

The implementation below maintains a pointer to a stream buffer which normally expected to live on the heap and will be released upon destruction with the help of std::unique_ptr<...>. As it may be desirable to return an std::omstream the stream buffer of a long-lived stream, e.g., std::cout, the std::unique_ptr<...> is set up to use a deleter which may do nothing if the omstream doesn't own the stream buffer.

#include <ostream>
#include <memory>
#include <utility>

template <typename cT, typename Traits = std::char_traits<cT>>
class basic_omstream
    : public std::basic_ostream<cT, Traits>
{
    using deleter = void (*)(std::basic_streambuf<cT, Traits>*);

    static void delete_sbuf(std::basic_streambuf<cT, Traits>* sbuf) {
        delete sbuf;
    }
    static void ignore_sbuf(std::basic_streambuf<cT, Traits>*) {
    }
    std::unique_ptr<std::basic_streambuf<cT, Traits>, deleter> m_sbuf;
public:
    basic_omstream()
        : std::basic_ios<cT, Traits>()
        , std::basic_ostream<cT, Traits>(nullptr)
        , m_sbuf(nullptr, &ignore_sbuf) {
    }
    basic_omstream(std::basic_streambuf<cT, Traits>* sbuf,
                   bool owns_streambuf)
        : std::basic_ios<cT, Traits>()
        , std::basic_ostream<cT, Traits>(sbuf)
        , m_sbuf(sbuf, owns_streambuf? &delete_sbuf: &ignore_sbuf) {
        this->set_rdbuf(this->m_sbuf.get());
    }
    basic_omstream(basic_omstream&& other)
        : std::basic_ios<cT, Traits>() // default construct ios!
        , std::basic_ostream<cT, Traits>(std::move(other))
        , m_sbuf(std::move(other.m_sbuf)) {
        this->set_rdbuf(this->m_sbuf.get());
    }
    basic_omstream& operator=(basic_omstream&& other) {
        this->std::basic_ostream<cT, Traits>::swap(other);
        this->m_sbuf.swap(other.m_sbuf);
        this->set_rdbuf(this->m_sbuf.get());
        return *this;
    }
};

typedef basic_omstream<char>    omstream;
typedef basic_omstream<wchar_t> womstream;

Using an std::ofstream or an std::ostringstream to initialize an omstream doesn't work unless the corresponding stream outlives the omstream. In general a corresponding stream buffer will be allocated. The class omstream could, e.g., be used like in the code below which create a stream based on an URI given to a suitable factory function:

#include <iostream>
#include <sstream>
#include <fstream>

omstream make_stream(std::string const& uri) {
    if (uri == "stream://stdout") {
        return omstream(std::cout.rdbuf(), false);
    }
    else if (uri == "stream://stdlog") {
        return omstream(std::clog.rdbuf(), false);
    }
    else if (uri == "stream://stderr") {
        return omstream(std::cerr.rdbuf(), false);
    }
    else if (uri.substr(0, 8) == "file:///") {
        std::unique_ptr<std::filebuf> fbuf(new std::filebuf);
        fbuf->open(uri.substr(8), std::ios_base::out);
        return omstream(fbuf.release(), true);
    }
    else if (uri.substr(0, 9) == "string://") {
        return omstream(new std::stringbuf(uri.substr(9)), true);
    }
    throw std::runtime_error("unknown URI: '" + uri + "'");
}

int main(int ac, char* av[])
{
    omstream out{ make_stream(ac == 2? av[1]: "stream://stdout") };
    out << "hello, world\n";
}

If there are other stream buffers available which could be constructed from a URI, these could be added to the make_stream() function.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
7

You've almost got it right. Your example is move constructing the ios base twice. You should move only the direct base class. And assuming there is member streambuf, move that too:

class omstream
    : public std::ostream {
    // suitable members
public:
    omstream(/* suitable constructor arguments */);
    omstream(omstream&& other) // follow recipe of 27.9.1.11 [ofstream.cons] paragraph 4
        : std: ostream(std::move(other)),
        // move any members {
        this->set_rdbuf(/* install the stream buffer */);
    }
    // other helpful or necessary members
};

I changed "get" to "install" in the set_rdbuf comment. Typically this installs a pointer to the member streambuf into the ios base class.

The current unorthodox design of the move and swap members of istream/ostream was set up to make the move and swap members of the derived classes (such as ofstream and omstream) more intuitive. The recipe is:

Move the base and members, and in the move constructor set the rdbuf.

It is that embedded rdbuf that is the complicating factor for the entire hierarchy.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 2
    I entirely missed that `std::basic_ostream<...>`'s move constructor calls `std::basic_ios<...>::move()`. That is is, the call to `std::basic_ios<...>` default constructor is actually not done from `std::basic_ostream<...>` but from the most derived type as it is a `virtual` base: the statement in 27.7.3.2 [ostream.cons] paragraph 5 to call `std::basic_ios<...>`'s default constructor is part of the contract of a deriving stream! – Dietmar Kühl Dec 25 '13 at 22:40
  • 4
    This is confusing territory. I have to re-learn it every time I look at it. – Howard Hinnant Dec 25 '13 at 22:59