21

The fwrite() function in C uses const void *restrict buffer as the first argument, so you can pass pointer to your struct as the first parameter directly.
http://en.cppreference.com/w/c/io/fwrite
e.g. fwrite(&someStruct, sizeof(someStruct), 1, file);

But in C++, the ostream::write() requires const char_type*, which forces you to use reinterpret_cast. (In Visual Studio 2013, it's const char*.)
http://en.cppreference.com/w/cpp/io/basic_ostream/write
e.g. file.write(reinterpret_cast<char*>(&someStruct), sizeof(someStruct));

In almost all cases, the binary data to be written to files is not a char array, so why does the standard prefer the style which seems more complex?

P.S.
1. Actually I used the write() method in ofstream with ios::binary mode, but according to the reference, it inherits ofstream. So I use ostream::write() above.
2. If you want to print a stream of characters, you could use operator<<(). Isn't write() method designed for writing raw data?
3. If write() is not the way to write binary data, then what is the way to do it within the standard? (Although this may bother portability of the code due to various memory align strategies on different platforms)

Mr. Ree
  • 871
  • 9
  • 20
  • 2
    Because this makes the cast *explicit*, and requires the programmer to think a moment about how converting his (binary) struct into a sequence of characters might affect portability? (This is me guessing, hence a comment and not an answer.) – DevSolar Feb 17 '15 at 09:24
  • 12
    You're asking why a stream of characters behaves like a stream of characters? Because it's a stream of characters, whether or not you want to (ab)use it by writing the raw bytes of other types into it. – Mike Seymour Feb 17 '15 at 09:29
  • 2
    Anything you can write to a file *is* a `char` array. – n. m. could be an AI Feb 17 '15 at 12:18
  • @DevSolar The problem is that if you have an `unsigned char` array - a type used to hold binary data, you cannot write that into an `fstream` without reinterpret_cast. And clang-tidy and other linters bite for that. – Calmarius Jul 27 '19 at 18:51

4 Answers4

13

The portrayal of this as a C vs C++ thing is misleading. C++ provides std::fwrite(const void*, ...) just like C. Where C++ chooses to be more defensive is specifically the std::iostream versions.

"Almost in all cases the binary data to be written to files is not char array"

That's debatable. In C++ isn't not unusual to add a level of indirection in I/O, so objects are streamed or serialised to a convenient - and possibly portable (e.g. endian-standardised, without or with standardised structure padding) - representation, then deserialised/parsed when re-read. The logic is typically localised with the individual objects involved, such that a top-level object doesn't need to know details of the memory layout of its members. Serialisation and streaming tends to be thought of / buffered etc. at the byte level - fitting in better with character buffers, and read() and write() return a number of characters that could currently be transmitted - again at the character and not object level - so it's not very productive to pretend otherwise or you'll have a mess resuming partially successful I/O operations.

Raw binary writes / reads done naively are a bit dangerous as they don't handle these issues so it's probably a good thing that the use of these functions is made slightly harder, with reinterpret_cast<> being a bit of a code smell / warning.

That said, one unfortunate aspect of the C++ use of char* is that it may encourage some programmers to first read to a character array, then use inappropriate casts to "reinterpret" the data on the fly - like an int* aimed at the character buffer in a way that may not be appropriately aligned.

If you want to print a stream of characters, you could use operator<<(). Isn't write() method designed for writing raw data?

To print a stream of characters with operator<<() is problematic, as the only relevant overload takes a const char* and expects a '\0'/NUL-terminated buffer. That makes it useless if you want to print one or more NULs in the output. Further, when starting with a longer character buffer operator<< would often be clumsy, verbose and error prone, needing a NUL swapped in and back around the streaming, and would sometimes be a significant performance and/or memory use issue e.g. when writing some - but not the end - of a long string literal into which you can't swap a NUL, or when the character buffer may be being read from other threads that shouldn't see the NUL.

The provided std::ostream::write(p, n) function avoids these problems, letting you specify exactly how much you want printed.

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • Thanks~ Maybe I should use "raw data" to replace "binary data" because all data in computer is binary... It's true that writing raw data is a dangerous behavior for a cross-platform application. But for applications on embedded systems which focus more on efficiency rather than portability, this may be a helpful approach :) – Mr. Ree Feb 18 '15 at 03:02
  • 1
    @Mr.Ree: for sure - the functions are there to be called - just ever-so-slightly harder to do so for `class`/`struct` types, needing that cast. Regarding the dangers - if there are virtual functions, pointer data members etc. you're in trouble even on an embedded system, but C++ provides [traits](http://en.cppreference.com/w/cpp/types/is_trivially_copyable) you can easily `assert` to ensure your `struct` data should be safe to `read`/`write` as a binary block.... – Tony Delroy Feb 18 '15 at 04:39
  • @Mr.Ree: appended a response to another of your questions to my answer. Cheers. – Tony Delroy Feb 18 '15 at 04:51
  • I agree with what you have written. But, Java has 3 different layers of classes for reading text from a file (a stream, a reader and a buffered reader). I am not defending their design decisions but this also shows that current `std::iostream` implements different responsibilities into one gigantic construct. Using `std::` objects are usually encouraged by Stroustrup himself just because of their RAII behavior. So instead of `fwrite` there should be a better alternative for current stream classes in the standard for byte streams imho. – zahir Feb 18 '15 at 16:58
  • @zahir: from one perspective I agree with you it "ticks a box", from another it seems excessive, verbose and confusing for new devs to have an extra set of stream classes that only differ by having `read` and/or `write` functions take `void*` instead of `char*` - the Standard is long enough already. Users who want it can trivially roll their own. Buffering variations are already supported - see [.rdbuf()](http://en.cppreference.com/w/cpp/io/basic_ios/rdbuf). – Tony Delroy Feb 19 '15 at 01:23
10

char_type is not exactly char *, it's the template parameter of the stream that represents the stream's character type:

template<typename _CharT, typename _Traits>
class basic_ostream : virtual public basic_ios<_CharT, _Traits>
{
public:
    // Types (inherited from basic_ios):
    typedef _CharT                  char_type;
    <...>

And std::ostream is just the char instantiation:

typedef basic_ostream<char> ostream;
SingerOfTheFall
  • 29,228
  • 8
  • 68
  • 105
1

In C/C++, char is the data type for representing a byte, so char[] is the natural data type for binary data.

Your question, I think, is better directed at the fact C/C++ was not designed to have distinct data types for "bytes" and "characters", rather than at the design of the stream libraries.

  • 1
    Talking about the imaginary language "C/C++" is often not helpful, especially when the difference between C and C++ is being examined. – Pete Becker Feb 17 '15 at 17:52
  • 2
    I disagree that `char[]` is the natural type for binary data. C and C++ both have messy properties about working with negative values; it is much easier to use `unsigned char` for binary data. – M.M Feb 18 '15 at 02:54
  • 2
    @Matt: I usually prefer `unsigned char` too if I'm thinking of a byte as a numeric type or a collection of bits rather than an opaque thing, but I don't think it's worth bringing up in this context. Incidentally, if you want to think of a byte as a signed numeric type, `char` is still unsuitable, because it's implementation defined whether or not `char` is a signed or an unsigned type! –  Feb 18 '15 at 05:42
-3

Ree,

From the cplusplus.com site the signature of ostream::write is :

ostream& write (const char* s, streamsize n);

I have just checked it on VS2013, you can write easily :

std::ofstream outfile("new.txt", std::ofstream::binary);
char buffer[] = "This is a string";
outfile.write(buffer, strlen(buffer));
thomas
  • 526
  • 3
  • 10