I'm working on a custom, template-heavy serialization library with custom serializers. I want to be able to detect and enforce the Serializer
concept in my library using SFINAE (I don't have access to a C++20 compiler with concepts support):
class CustomSerializer
{
static T Serialize(S);
static S Deserialize(T);
};
The idea here is that the input type of Serialize
must be equal to the output type of Deserialize
, and vice versa
Is this possible? If so, how?
I tried looking into std::invoke_result_t
, but then you need to provide the argument types. But the argument type of Deserialize is the invoke result of Serialize, and to get the invoke result of Serialize, ...
You see the circular pattern here, I hope, which makes me wonder if it's even possible.
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);
}
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;
}
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.
Serialize
exists for a specific type, and if the value returned by Deserialize
can be converted into that typeThis 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
).
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 {};
}
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;
};
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]};
}
};
Introspection of a set of overloaded function or of a template function is rather limited. But in the case where Serialize
and Deserialize
would not be overloaded, it is possible to get the return type and argument type of these functions, for example:
template<class Arg,class Ret>
auto get_arg_type(Ret(Arg)) -> Arg;
template<class Arg,class Ret>
auto get_ret_type(Ret(Arg)) -> Ret;
class CustomSerializer
{
public:
static int Serialize(double);
static double Deserialize(int);
};
//Concept check example:
static_assert(std::is_same_v<decltype(get_arg_type(CustomSerializer::Serialize))
,decltype(get_ret_type(CustomSerializer::Deserialize))>);
static_assert(std::is_same_v<decltype(get_ret_type(CustomSerializer::Serialize))
,decltype(get_arg_type(CustomSerializer::Deserialize))>);
In my opinion this solution is not adequate because the concept will fail for wrong reasons (when Serialize
or Deserialize
are template and or overloaded).
A mitigation could be to use a trait, so the user may be able to specialize the trait when its type provides functionalities not taken into account in your library:
class CustomSerializer
{
public:
static int Serialize(double);
static int Serialize(char);
static double Deserialize(int);
};
template<class T>
struct serialize_from{
using type = decltype(get_arg_type(T::Serialize));
};
template<class T>
using serialize_from_t = typename serialize_from<T>::type;
template<>
struct serialize_from<CustomSerializer>{
using type = double;
};
//The concept check use the trait:
static_assert(std::is_same_v<decltype(CustomSerializer::Deserialize(
CustomSerializer::Serialize(
std::declval<const serialize_from_t<CustomSerializer>&>())))
,serialize_from_t<CustomSerializer>>);
//N.B.: You should also provide a serialize_to trait, here the concept
//check a convertibility where you expect a type equality... but the code
//would be too long for this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With