Writing binary data to a file in C is simple: use fwrite, passing the address of the object you want to write and the size of the object. Is there something more "correct" for Modern C++ or should I stick to using FILE* objects? As far as I can tell the IOStream library is for writing formatted data rather than binary data, and the write member asks for a char* leaving me littering my code with casts.
2 Answers
So the game here is to enable argument dependent lookup on reading and writing, and make sure you don't try to read/write things that are not flat data.
It fails to catch data containing pointers, which also should not be read/written this way, but it is better than nothing
namespace serialize { namespace details { template<class T> bool write( std::streambuf& buf, const T& val ) { static_assert( std::is_standard_layout<T>{}, "data is not standard layout" ); auto bytes = sizeof(T); return buf.sputn(reinterpret_cast<const char*>(&val), bytes) == bytes; } template<class T> bool read( std::streambuf& buf, T& val ) { static_assert( std::is_standard_layout<T>{}, "data is not standard layout" ); auto bytes = sizeof(T); return buf.sgetn(reinterpret_cast<char*>(&val), bytes) == bytes; } } template<class T> bool read( std::streambuf& buf, T& val ) { using details::read; // enable ADL return read(buf, val); } template<class T> bool write( std::streambuf& buf, T const& val ) { using details::write; // enable ADL return write(buf, val); } } namespace baz { // plain old data: struct foo {int x;}; // not standard layout: struct bar { bar():x(3) {} operator int()const{return x;} void setx(int s){x=s;} int y = 1; private: int x; }; // adl based read/write overloads: bool write( std::streambuf& buf, bar const& b ) { bool worked = serialize::write( buf, (int)b ); worked = serialize::write( buf, b.y ) && worked; return worked; } bool read( std::streambuf& buf, bar& b ) { int x; bool worked = serialize::read( buf, x ); if (worked) b.setx(x); worked = serialize::read( buf, b.y ) && worked; return worked; } } I hope you get the idea.
Possibly you should restrict said writing based off is_pod not standard layout, with the idea that if something special should happen on construction/destruction, maybe you shouldn't be binary blitting the type.
3 Comments
serialize::read explicitly and it will still find a local namespace read override for a type. Easier than forcing end users to always be ADL friendly when calling read, so I did the work for them.std::tuple<Ts&...> overload in details. Then aggregates can read(stream,std::tie(my_members...)) and poof, done.Since you are already bypassing all formatting, I would recommend using the std::filebuf class directly to avoid possible overheads from std::fstream; it's definitely better than FILE* due to RAII.
You can't escape from the casts this way, sadly. But it's not hard to wrap it, like:
template<class T> void write(std::streambuf& buf, const T& val) { std::size_t to_write = sizeof val; if (buf.sputn(reinterpret_cast<const char*>(&val), to_write) != to_write) // do some error handling here }
std::basic_ostream::writeandstd::basic_istream::readfunctions to write or read e.g. structures or other binary/raw data. And yes, you have to do type-casting to make it work.readandwritesuch that te wrappers acceptvoid*rather thanchar*, and add error handling while you're at it.