Simple solution - check if function pointers are co-dependent
This is actually really simple to do through pattern matching. We can write a constexpr
function, which I'll call checkInverse
, which returns true if the types are inverted, and false otherwise:
template<class S, class T>
constexpr bool checkInverse(S(*)(T), T(*)(S)) {
return true;
}
template<class S, class T, class Garbage>
constexpr bool checkInverse(S(*)(T), Garbage) {
return false;
}
Because the first case is more specialized, if it's satisfied then the function will return true, and otherwise it'll return false.
We can then use this to check if a class's Serialize
and Deserialize
methods match each other:
template<class T>
constexpr bool isValidPolicy() {
return checkInverse(T::Serialize, T::Deserialize);
}
What if we're not sure that the class has a Serialize
and Deserialize
method?
We can expand the isValidPolicy
to check that using SFINAE. Now, it'll only return true if those methods exist, AND they satisfy the type co-dependency.
If I call isValidPolicy<Type>(0)
, then it'll attempt to use the int
overload. If Serialize
and Deserialize
don't exist, it'll fall back to the long
overload, and return false.
template<class Policy>
constexpr auto isValidPolicy(int)
-> decltype(checkInverse(Policy::Serialize, Policy::Deserialize))
{
return checkInverse(Policy::Serialize, Policy::Deserialize);
}
template<class Policy>
constexpr auto isValidPolicy(long)
-> bool
{
return false;
}
What are the cons of this solution?
On the face of it, this seems like a good solution, although it does have a few issues. If Serialize
and Deserialize
are templated, it won't be able to do the conversion to a function pointer.
In addition, future users might want to write Deserialize
methods that return an object that can be converted into the serialized type. This could be extremely useful for directly constructing an object into a vector without copying, improving efficiency. This method won't allow Deserialize
to be written that way.
Advanced solution - check if Serialize
exists for a specific type, and if the value returned by Deserialize
can be converted into that type
This solution is more general, and ultimately more useful. It enables a good deal of flexibility with the way Serialize
and Deserialize
are written, while ensuring certain constraints (namely that Deserialize(Serialize(T))
can be converted to T
).
Checking that the output is convertible to some type
We can use SFINAE to check this, and wrap it into a is_convertable_to
function.
#include <utility>
#include <type_traits>
template<class First, class... T>
using First_t = First;
template<class Target, class Source>
constexpr auto is_convertable_to(Source const& source, int)
-> First_t<std::true_type, decltype(Target(source))>
{
return {};
}
template<class Target, class Source>
constexpr auto is_convertable_to(Source const& source, long)
-> std::false_type
{
return {};
}
Checking if a type represents a valid Serializer
We can use the above conversion checker to do this. This will check it for a given type, which has to be passed as a parameter to the template. The result is given as a static bool constant.
template<class Serializer, class Type>
struct IsValidSerializer {
using Serialize_t =
decltype(Serializer::Serialize(std::declval<Type>()));
using Deserialize_t =
decltype(Serializer::Deserialize(std::declval<Serialize_t>()));
constexpr static bool value = decltype(is_convertable_to<Type, Deserialize_t>(std::declval<Deserialize_t>(), 0))::value;
};
An example of a lazy deserializer
I mentioned before that it's possible to rely on overlading the conversion operator for serialization / deserialization. This is an extremely powerful tool, and it's one we can use to write lazy serializers and deserializers. For example, if the serialized representation is a std::array
of char
, we could write the lazy deserializer like so:
template<size_t N>
struct lazyDeserializer {
char const* _start;
template<class T>
operator T() const {
static_assert(std::is_trivially_copyable<T>(), "Bad T");
static_assert(sizeof(T) == N, "Bad size");
T value;
std::copy_n(_start, N, (char*)&value);
return value;
}
};
Once we have that, writing a Serialize
policy that works with any trivially copyable type is relatively straight-forward:
#include <array>
#include <algorithm>
class SerializeTrivial {
public:
template<class T>
static std::array<char, sizeof(T)> Serialize(T const& value) {
std::array<char, sizeof(T)> arr;
std::copy_n((char const*)&value, sizeof(T), &arr[0]);
return arr;
}
template<size_t N>
static auto Deserialize(std::array<char, N> const& arr) {
return lazyDeserializer<N>{&arr[0]};
}
};
decltype
on the function pointers? – Laminated