Why Are iOStreams Not Copyable

Why are iostreams not copyable?

Copying and moving are value-semantic operations. To define them, you first have to decide what properties of a class give its objects distinct values. This point was at first largely sidestepped for the iostreams library, and then C++11 took a different direction incompatible with such a copy constructor.

The state of a stream object comprises two parts: A pointer to a stream buffer with its associated state, and formatting information. Since C++98, rdbuf, rdstate, and copyfmt expose this information separately.

Since C++11, stream classes also have a protected interface including a move constructor (and a member called move) which copies the format but not the stream buffer pointer. This commits iostream to treating the formatting information exclusively as the state of the stream object.

If streams were made copyable at this point, it would only do copyfmt and not the rest.

The choice to exclude rdbuf from the value state may be due to the further-muddled value semantics of derived classes such as std::fstream, which not only expose access to a stream buffer, but also embed and own it.

std::ifstream f( path + filename ); // Owns, or even "is," a file.
std::istream i = f; // Observes an externally-managed file.

std::istream i2 = i; // OK, copy a shallow reference.
std::ifstream f2 = f; // Error, ifstream is more than a shallow reference.

std::istream i3 = std::move( f ); // Error? Would retain a reference to an rvalue.
std::ifstream f3 = std::move( f ); // OK: full copy including the file buffer.

The semantics could be consistent in some fashion, but it would be a lot of confusion for a moderate gain.

If iostream objects are not copyable, why is the following code legal?

See this page. When you say

return file;

file is an "id-expression" (i.e. it's the name of some variable). These have special handling under return:

automatic move from local variables and parameters


If expression is a (possibly parenthesized) id-expression that names a variable ... then overload resolution to select the constructor to use for initialization of the returned value ... is performed twice:

  • first as if expression were an rvalue expression (thus it may select the move constructor), ...
  • then overload resolution is performed as usual, with expression considered as an lvalue (so it may select the copy constructor).

You cannot copy an ifstream, that's true, but you can move it. Therefore, the compiler essentially inserts an implicit std::move for you:

return std::move(file);

which allows the move constructor of ifstream to be called, even though the copy constructor is deleted. "Moving" means that any resources "owned" by the istream are transferred to a new object. The move constructor takes ownership of these resources away from the source object (thus modifying it), while giving it to the new object. Contrast the copy constructor, which we generally suppose should not modify the source and so couldn't take its ownership away. This makes a copy constructor unsafe for istream, since it would mean two objects trying to manage one external resource and possibly confusing each other. A move constructor is not bound by the contract to leave the source object untouched, so it can ensure that resources are only owned by one object.

cannot assign or copy iostream object?

Your code attempts to turn one istream, the one passed to the constructor, into two istreams, the one that was passed to the constructor and ais. An istream object represents that actual stream itself. There is only one stream and there is no way to somehow turn it into two streams.

It's not even clear what that would mean. If there's some data on the stream, does whichever stream reads first get it? Or do they both get it? If so, who or what duplicates it?

An istream is like a file itself. You cannot turn one file into two files without copying the data from one to the other yourself. You can, however, have as many references or pointers to the same istream as you want. The solution to your problem is probably to make ais a reference.

Boost library iostream::copy not working

The error you are getting is actually misleading.

This is because:

if(boost::iostreams::bzip2::config_error)

...always evaluates to non-zero and gives you the wrong error message!

I don't have a Windows PC available to me, but I tested "your" code on a Mac (Boost installed using Homebrew), and got the same error.

libbzip2 has been improperly configured for the current platform

And so I fixed that up and added some error checking:

#include <fstream>
#include <iostream>
#include <boost/iostreams/filtering_streambuf.hpp>
#include <boost/iostreams/copy.hpp>
#include <boost/iostreams/filter/bzip2.hpp>
#include <boost/filesystem.hpp>

int main()
{
using namespace std;
using namespace boost::iostreams;

char filename[] = "gcc-4.7.2.tar.bz2";

if (!boost::filesystem::exists(filename))
{
cout << "Can't find " << filename << ". Expect errors to follow! " << endl;
}

ifstream file(filename, ios_base::in | ios_base::binary);

filtering_streambuf<input> in;
in.push(bzip2_decompressor());
in.push(file);
try
{
boost::iostreams::copy(in, cout);
}
catch (const bzip2_error& exception)
{
cout << exception.what() << endl;

int error = exception.error();
if (error == bzip2::data_error)
{
cout << "compressed data stream is corrupted";
}
else if (error == bzip2::data_error_magic)
{
cout << "compressed data stream does not begin with the 'magic' sequence 'B' 'Z' 'h'";
}
else if (error == bzip2::config_error)
{
cout << "libbzip2 has been improperly configured for the current platform";
}
else
{
cout << "Error: " << error;
}
cout << endl;
}
}

Build

> clang++ 18121908.cpp -lboost_iostreams -lboost_system -lboost_filesystem -o 18121908

Run

> ./18121908 
Can't find gcc-4.7.2.tar.bz2. Expect errors to follow!
bzip2 error
Error: -7

Why -7? Because #define BZ_UNEXPECTED_EOF (-7). Boost calls this bzip2::unexpected_eof. You've unexpectedly hit the end of the stream!

Make a valid (if questionable) bzip2 file:

> echo 'There should be some compiler stuff in here' > gcc-4.7.2.tar
> bzip2 gcc-4.7.2.tar

Re-run

> ./18121908 
There should be some compiler stuff in here

Conclusion

You need to check whether the file exists before attempting to decompress it.

boost::iostreams::copy() closes the source but not the sink

Interesting.

Going down the rabbit hole[1] it turns out that close_impl<any_tag> is finally reached for the ofstream wrapped deep inside the chain_buf inside the filtering_streambuf. The implementation reads:

template<>
struct close_impl<any_tag> {
template<typename T>
static void close(T& t, BOOST_IOS::openmode which)
{
if (which == BOOST_IOS::out)
iostreams::flush(t);
}

template<typename T, typename Sink>
static void close(T& t, Sink& snk, BOOST_IOS::openmode which)
{
if (which == BOOST_IOS::out) {
non_blocking_adapter<Sink> nb(snk);
iostreams::flush(t, nb);
}
}
};

So, as you can see, the documented behaviour is actually just that the linked output stream buffer(s) are flushed (there's also a synch on the containing entity prior to that call, IIRC).

I completely agree that this could have been made a whole lot more explicit.

Reading the TMP code that decides on the specialization:

template<typename T>
struct close_tag {
typedef typename category_of<T>::type category;
typedef typename detail::unwrapped_type<T>::type unwrapped;
typedef typename
iostreams::select<
mpl::not_< is_convertible<category, closable_tag> >,
any_tag,
mpl::or_<
is_boost_stream<unwrapped>,
is_boost_stream_buffer<unwrapped>
>,
close_boost_stream,
mpl::or_<
is_filtering_stream<unwrapped>,
is_filtering_streambuf<unwrapped>
>,
close_filtering_stream,
mpl::or_<
is_convertible<category, two_sequence>,
is_convertible<category, dual_use>
>,
two_sequence,
else_,
closable_tag
>::type type;
};

Several workarounds come to mind:

  1. define a specialization of close_tag<> for std::ofstream that actually returns a different tag and make it so that it gets closed (I recommend against this since it can have unintended effects by going against the assumptions held by the devs of Boost Iostreams)

  2. use a boost class for the output stream:

#include <iostream>
#include <fstream>

#include <boost/iostreams/filtering_streambuf.hpp>
#include <boost/iostreams/copy.hpp>
#include <boost/iostreams/device/file.hpp>
#include <boost/iostreams/filter/gzip.hpp>

using namespace std;

int main(void)
{
cout << boolalpha;

ifstream ifs("output", ios::binary);
boost::iostreams::file_sink ofile("output.boost.gz");

boost::iostreams::filtering_streambuf<boost::iostreams::output> out;
out.set_auto_close(true);

out.push(boost::iostreams::gzip_compressor());
out.push(ofile);

cout << "out.is_complete(): " << out.is_complete() << endl;
cout << "ifs.is_open()? " << ifs.is_open() << endl;
cout << "ofile.is_open()? " << ofile.is_open() << endl;

boost::iostreams::copy(ifs, out);

cout << "out.is_complete(): " << out.is_complete() << endl;
cout << "ifs.is_open()? " << ifs.is_open() << endl;
cout << "ofile.is_open()? " << ofile.is_open() << endl;
}

See it Live on Coliru


[1] It is a surprisingly large rabbit hole, I must add. I wonder what benefit all this genericity really has

compile error on boost::iostreams::copy

My copy of clang fails to compile that as well, telling me that note: candidate function [snip] not viable: expects an l-value for 2nd argument.

That seems pretty reasonable to me, and, in fact, this compiles:

boost::regex reg("a.c");    
string str("abcdef aochijk");
string result;
boost::iostreams::filtering_ostream ios(
boost::iostreams::regex_filter(reg,"test") |
boost::iostreams::back_inserter(result));
boost::iostreams::copy( boost::make_iterator_range(str), ios);

Using boost iostreams filters (close and non-copyable)

The first problem is that your sha_output_filter does not meet the requirements for using the non-const overload of push, because it is not derived from std::istream, std::ostream or std::streambuf, so it is not classified as a standard stream or stream buffer type.

This can be deduced from one of the first messages from the compiler

test.cpp: In function ‘T boost::iostreams::detail::wrap(const T&, typename boost::disable_if<boost::iostreams::is_std_io<T> >::type*) [with T = sha_output_filter<CryptoPP::SHA1>, typename boost::disable_if<boost::iostreams::is_std_io<T> >::type = void]’:

where it indicates that it can successfully resolve the boost::disable_if<...>::type, so it does not disable this overload. If you look at the source code, you will probably find a enable_if test on the non-const overload.


Regarding the second problem, your filter is not marked as being closable, so the Boost library does not know it can call close on the filter.

This can be resolved by replacing the typedef for category with

struct category : boost::iostreams::output_filter_tag, boost::iostreams::closable_tag {};

Why is a public copy constructor required even if it is not invoked?

Here are the relevant bits of the C++ standard that are involved:

[dcl.init]/16, bullet 6, sub-bullet 1: If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered.... If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed. [emphasis added]

In other words, it doesn't matter if a compiler optimization could elide the copy, the initialization is ill-formed because there are no applicable constructors. Of course, once you make the copy constuctor public, the following section applies:

[class.copy]/31: When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.... This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

bullet 3: when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move



Related Topics



Leave a reply



Submit