Detecting a pattern of co-dependent functions with SFINAE

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]}; 
    }
};