【问题标题】:Detecting a pattern of co-dependent functions with SFINAE使用 SFINAE 检测相互依赖的函数模式
【发布时间】:2019-05-06 04:20:10
【问题描述】:

我正在使用自定义序列化程序开发一个自定义的、大量模板的序列化库。我希望能够使用 SFINAE 在我的库中检测和执行 Serializer 概念(我无法访问支持概念的 C++20 编译器):

class CustomSerializer
{
    static T Serialize(S);
    static S Deserialize(T);
};

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

这可能吗?如果有,怎么做?

我尝试查看std::invoke_result_t,但是您需要提供参数类型。但是Deserialize的参数类型是Serialize的调用结果,要得到Serialize的调用结果,...

我希望你看到这里的圆形图案,这让我怀疑它是否可能。

【问题讨论】:

  • 如果类不重载这些名称或将它们作为函数对象提供,你就不能在函数指针上使用decltype吗?
  • @DavisHerring 你能举个例子吗?
  • 我或多或少在想 Oliv 的回答(第一部分)。

标签: c++ sfinae typetraits


【解决方案1】:

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

这实际上通过模式匹配非常简单。我们可以编写一个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;
}

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

然后我们可以使用它来检查一个类的 SerializeDeserialize 方法是否相互匹配:

template<class T>
constexpr bool isValidPolicy() {
    return checkInverse(T::Serialize, T::Deserialize); 
}

如果我们不确定该类是否有 SerializeDeserialize 方法?

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

如果我调用isValidPolicy&lt;Type&gt;(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; 
}

这个解决方案有什么缺点?

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

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

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

这个解决方案更通用,最终也更有用。它为SerializeDeserialize 的编写方式提供了很大的灵活性,同时确保了某些约束(即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 {}; 
}

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

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

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

惰性反序列化器示例

我之前提到过,可以依靠重载转换运算符来进行序列化/反序列化。这是一个非常强大的工具,我们可以用它来编写惰性序列化器和反序列化器。例如,如果序列化的表示是 charstd::array,我们可以这样编写惰性反序列化器:

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

一旦我们有了它,编写一个适用于任何可简单复制的类型的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]}; 
    }
};

【讨论】:

  • 非常详细的回复。非常感谢!
【解决方案2】:

对一组重载函数或模板函数的自省是相当有限的。但是在SerializeDeserialize不会被重载的情况下,可以得到这些函数的返回类型和参数类型,例如:

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))>);

在我看来,这个解决方案是不够的,因为这个概念会因为错误的原因而失败(当SerializeDeserialize 是模板和/或重载时)。

缓解措施可能是使用特征,因此当特征的类型提供您的库中未考虑的功能时,用户可以专门化该特征:

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.

【讨论】:

    猜你喜欢
    • 2011-01-25
    • 2013-04-06
    • 1970-01-01
    • 2012-03-28
    • 1970-01-01
    • 1970-01-01
    • 2014-01-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多