4

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.

5
  • 3
    You use the std::basic_ostream::write and std::basic_istream::read functions to write or read e.g. structures or other binary/raw data. And yes, you have to do type-casting to make it work. Commented Feb 26, 2015 at 14:49
  • @JoachimPileborg this should be an answer. Commented Feb 26, 2015 at 14:52
  • These casts are a friendly reminder that you are reinterpreting your data as a sequence of bytes. If you don't like the litter, wrap read and write such that te wrappers accept void* rather than char*, and add error handling while you're at it. Commented Feb 26, 2015 at 14:54
  • stackoverflow.com/questions/7349689/… Commented Feb 26, 2015 at 15:00
  • See this mine answer to a related question. Commented Feb 26, 2015 at 21:11

2 Answers 2

5

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.

live example.

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.

Sign up to request clarification or add additional context in comments.

3 Comments

That's looking good, but before I accept, can you tell me why you have declared a forwarding function?
@hatcat easy ADL. Call 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.
@hatcat as an aside, have a not standard layout sfinae tested std::tuple<Ts&...> overload in details. Then aggregates can read(stream,std::tie(my_members...)) and poof, done.
2

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
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.