We have some code that looks like this:
class Serializer
{
public:
template<class Type> void Write(const Type& value)
{
internal_write((byte*)value, sizeof(Type));
}
// some overloads of Write that take care of some tricky type we have defined
private:
// implementation of internal_write
};
As one might guess this writes data to a disk. We have similar Read functions that more or less cast some bytes to a type. Not as robust as it could be, but it works, because we write on the same platform as we read–meaning that the bytes we write match what we need to read.
We are now moving to support multiple platforms. We have in several places code like:
unsigned long trust_me_this_will_be_fine = get_unsigned_long();
a_serializer.Write(trust_me_this_will_be_fine);
That works fine in the current world, but if we assume that one of the platforms we want to support has unsigned long as 32-bit and on another they are 64-bit, we are hosed.
I’d like to change Serializer::Write to only take explicitly-sized types as parameters. I thought about this:
class Serializer
{
public:
void Write(uint32_t value) { ... }
void Write(uint64_t value) { ... }
};
But I don’t think that really solves the problem because on the 32-bit system, an unsigned long will be automatically converted into a uint32_t but on a 64-bit system it will be auto-converted into a uint64_t.
What I really want here is to make Write(uint32_t) only accept parameters of type uint32_t–meaning that it would require an explicit cast. I don’t think there’s a direct way to do that–if I am wrong, please tell me.
Short of that, I can think of two ways to solve this.
- Declare (but don’t define) private versions of
Serializer::Writefor every type that could be auto-converted into a type we support. - Don’t take a
uint32_tdirectly, but a class that holds auint32_tand only has an explicit constructor for auint32_t.
Option 2 would look something like this:
class only_uint32
{
public:
uint32_t _value;
explicit only_uint32(uint_32 value) : _value(value) { }
};
class Serializer
{
public:
void Write(only_uint32 value) { ... }
};
Then calling code looks like this:
unsigned long might_just_work = get_unsigned_long();
a_serializer.Write(static_cast<uint32_t>(might_just_work)); // should work, and be explicitly sized.
a_serializer.Write(might_just_work); // won't compile
I assume many people have solved this type of problem. Is there a preferred way of doing this that I haven’t thought of? Is one of my ideas terrible, great, workable, anything?
P.S.: Yes, I understand this is a super-long post. It’s a somewhat complex and detailed problem, though.
Update:
Thanks for the ideas and help. I think we’re going with solution that like this:
class Serializer
{
public:
template<class Type> void Write32(const Type& value)
{
static_assert(sizeof(Type) == 4, "Write32 must be called on a 32-bit value.");
internal_write(reinterpret_cast<byte*>(value), 4);
}
// overloads like Write64 and various tricky types as before.
private:
// implementation of internal_write
};
This is a fairly low-cost (in terms of engineering time) solution that succeeds at pushing knowledge of what you’re actually saving out to the caller and forces the caller to know what they are calling.
You could do something like this:
Or if you want to accept only
uint32_ts: