Is it safe to serialize POD data by casting directly to char array?

Viewed 1186

Suppose that T is a POD type that doesn't contain a pointer, and I want to serialize T (in addition to some other data too). I've created the below functions to do this:

template<class T> void serialize(const T& source, char*& dest)
{
  *(T*)dest = source;
  dest += sizeof(T);
}
template<class T> void deserialize(T& dest, char*& source)
{
  dest = *(T*)source;
  source += sizeof(T);
}

Will this cause any problems, or are there any compilers where this won't work? In other words, will the code:

template<class T> bool check_sanity(const T& obj)
{
  std::unique_ptr<char[]> buffer { new int[sizeof(T)] };
  serialize(obj, buffer);
  T new_obj;
  deserialize(new_obj, buffer); 
  return new_obj == obj;
}

Ever return false? (Assume T is POD and no one's overloaded the == operator).

I'm writing these serialization methods for use in conjunction with MPI, where they'll be used at the start of the program to distribute some of the data needed for bookkeeping, so the same program will always be serializing and deserializing data.

3 Answers

I see a couple of problems. A minor one:

*(T*)dest = source;

IIRC, this is UB, because of aliasing rules violation (char * can alias any other pointer, but this means that you can access some object via a char * pointer, but not vice-versa, as in your example).

In other words, will the code: ... Ever return false?

Probably not, but you mentioned serializing not just a single object.

So, the major problem is alignment:

std::unique_ptr<char[]> buffer { new char[sizeof(int) + 1] };
char x = 0;
int y = 0;
serialize(x, buffer);
serialize(y, buffer); // may crash or write into wrong location

The faulty line is the same (but deserialize is also affected):

*(T*)dest = source; // source is int, dest is not aligned

The compiler will assume that dest is properly aligned and use CPU instructions for aligned store (on ARM architectures this will cause real problems).

The solution is to use memcpy instead:

memcpy(dest, &source, sizeof(T));

There is not need to worry about performance. Modern compilers are able to optimize memcpy of objects with known sizes very well.

*(T*)dest = source; is a strict aliasing violation.

Instead you should write:

memcpy(dest, &source, sizeof source);

You can copy POD objects around successfully using memcpy.

In your check_sanity function it will fail to compile, since operator== is not defined for T. (There are no implicitly-generated comparison operators)

Yes, you can do it as long as the buffer is an array of char, unsigned char or std::byte, C++ standard [basic.type]:

For any object (other than a base-class subobject) of trivially copyable type T , whether or not the object holds a valid value of type T, the underlying bytes (4.4) making up the object can be copied into an array of char, unsigned char, orstd::byte (21.2.1). If the content of that array is copied back into the object, the object shall subsequently hold its original value. [Example:

#define N sizeof(T)
char buf[N];
T obj; //obj initialized to its original value
std::memcpy(buf, &obj, N);// between these two calls to std::memcpy,obj might be modified
std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type holds its original value

— end example]

Nota: There are no requirement on the alignment of the buffer.

Related