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
)?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With