使用SFINAE检测共同依赖功能的模式

Zee*_*bit 8 c++ sfinae type-traits

我正在使用自定义序列化程序创建自定义,模板繁重的序列化库。我希望能够Serializer使用SFINAE在我的库中检测和强制执行该概念(我无法访问具有概念支持的C ++ 20编译器):

class CustomSerializer
{
    static T Serialize(S);
    static S Deserialize(T);
};
Run Code Online (Sandbox Code Playgroud)

这里的想法是的输入类型Serialize必须等于的输出类型Deserialize,反之亦然

这可能吗?如果是这样,怎么办?

我尝试研究std::invoke_result_t,但随后需要提供参数类型。但是Deserialize的参数类型是Serialize的调用结果,并且要获取Serialize的调用结果,...

我希望您在这里看到圆形图案,这使我想知道是否有可能。

J. *_*rez 3

简单的解决方案 - 检查函数指针是否相互依赖

实际上,通过模式匹配可以很简单地做到这一点。我们可以编写一个constexpr函数,我将其称为checkInverse,如果类型反转则返回 true ,否则返回 false :

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;
}
Run Code Online (Sandbox Code Playgroud)

因为第一种情况比较特殊,如果满足则函数将返回 true,否则将返回 false。

然后我们可以用它来检查类SerializeDeserialize方法是否彼此匹配:

template<class T>
constexpr bool isValidPolicy() {
    return checkInverse(T::Serialize, T::Deserialize); 
}
Run Code Online (Sandbox Code Playgroud)

如果我们不确定该类是否有SerializeandDeserialize方法怎么办?

isValidPolicy我们可以使用 SFINAE扩展以进行检查。现在,只有这些方法存在并且它们满足类型相互依赖关系时,它才会返回 true。

如果我调用isValidPolicy<Type>(0),那么它将尝试使用int重载。如果SerializeDeserialize不存在,它将回退到long重载,并返回 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; 
}
Run Code Online (Sandbox Code Playgroud)

该解决方案有哪些缺点?

从表面上看,这似乎是一个很好的解决方案,尽管它确实存在一些问题。如果SerializeDeserialize是模板化的,它将无法转换为函数指针。

此外,未来的用户可能希望编写Deserialize返回可转换为序列化类型的对象的方法。这对于直接将对象构建为向量而不进行复制非常有用,从而提高了效率。该方法不允许Deserialize以这种方式编写。

高级解决方案 - 检查Serialize特定类型是否存在,以及返回的值是否Deserialize可以转换为该类型

这个解决方案更通用,并且最终更有用。Serialize它使编写方式和Deserialize编写方式具有很大的灵活性,同时确保某些约束(即Deserialize(Serialize(T))可以转换为T)。

检查输出是否可转换为某种类型

我们可以使用 SFINAE 来检查这一点,并将其包装到一个is_convertable_to函数中。

#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 {}; 
}
Run Code Online (Sandbox Code Playgroud)

检查类型是否代表有效的序列化器

我们可以使用上面的转换检查器来做到这一点。这将检查给定类型,该类型必须作为参数传递给模板。结果以静态布尔常量形式给出。

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; 
};
Run Code Online (Sandbox Code Playgroud)

惰性反序列化器的示例

我之前提到过,可以依赖覆盖转换运算符来进行序列化/反序列化。这是一个非常强大的工具,我们可以用它来编写惰性序列化器和反序列化器。例如,如果序列化表示是 a std::arrayof char,我们可以像这样编写惰性反序列化器:

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; 
    }
};
Run Code Online (Sandbox Code Playgroud)

一旦我们有了这些,编写一个Serialize适用于任何可简单复制类型的策略就相对简单了:

#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]}; 
    }
};
Run Code Online (Sandbox Code Playgroud)