使用 SFINAE 检测相互依赖的函数模式
Detecting a pattern of co-dependent functions with SFINAE
我正在使用自定义序列化程序开发一个自定义的、大量使用模板的序列化库。我希望能够使用 SFINAE 在我的库中检测和执行 Serializer
概念(我无法访问支持概念的 C++20 编译器):
class CustomSerializer
{
static T Serialize(S);
static S Deserialize(T);
};
这里的思路是Serialize
的输入类型必须等于Deserialize
的输出类型,反之亦然
这可能吗?如果是,怎么做?
我尝试查看 std::invoke_result_t
,但您需要提供参数类型。但是Deserialize的参数类型是Serialize的调用结果,要得到Serialize的调用结果,...
我希望你看到这里的圆形图案,这让我想知道它是否可能。
对一组重载函数或模板函数的自省相当有限。但是在Serialize
和Deserialize
不会被重载的情况下,可以得到这些函数的return类型和参数类型,例如:
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))>);
在我看来,这个解决方案是不够的,因为这个概念会因为错误的原因而失败(当 Serialize
或 Deserialize
是模板和/或重载时)。
缓解措施可能是使用特征,因此当特征的类型提供您的库中未考虑的功能时,用户可以专门化该特征:
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.
简单的解决方案 - 检查函数指针是否相互依赖
这实际上通过模式匹配非常简单。我们可以编写一个 constexpr
函数,我将其称为 checkInverse
,如果类型反转,则 return 为真,否则为假:
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;
}
因为第一种情况更特殊,如果满足则函数将 return 为真,否则为 return 假。
然后我们可以使用它来检查 class 的 Serialize
和 Deserialize
方法是否相互匹配:
template<class T>
constexpr bool isValidPolicy() {
return checkInverse(T::Serialize, T::Deserialize);
}
如果我们不确定 class 有 Serialize
和 Deserialize
方法怎么办?
我们可以展开 isValidPolicy
来使用 SFINAE 检查。现在,只有 return 如果这些方法存在并且它们满足类型相互依赖性,它才会为真。
如果我调用 isValidPolicy<Type>(0)
,那么它将尝试使用 int
重载。如果 Serialize
和 Deserialize
不存在,它将回退到 long
重载和 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;
}
此解决方案的缺点是什么?
从表面上看,这似乎是一个很好的解决方案,尽管它确实存在一些问题。如果 Serialize
和 Deserialize
是模板化的,它将无法转换为函数指针。
此外,未来的用户可能希望编写 Deserialize
方法,使 return 对象可以转换为序列化类型。这对于直接将对象构造成向量而不进行复制非常有用,可以提高效率。此方法不允许 Deserialize
以这种方式编写。
高级解决方案 - 检查特定类型是否存在 Serialize
,以及 Deserialize
编辑的值 return 是否可以转换为该类型
这个解决方案更通用,最终也更有用。它使 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 {};
}
检查类型是否表示有效的序列化程序
我们可以使用上面的转换检查器来做到这一点。这将检查它的给定类型,该类型必须作为参数传递给模板。结果作为静态布尔常量给出。
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;
};
惰性反序列化器示例
我之前提到过,可以依靠覆盖转换运算符来进行序列化/反序列化。这是一个非常强大的工具,我们可以用它来编写惰性序列化器和反序列化器。例如,如果序列化表示是 char
的 std::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]};
}
};
我正在使用自定义序列化程序开发一个自定义的、大量使用模板的序列化库。我希望能够使用 SFINAE 在我的库中检测和执行 Serializer
概念(我无法访问支持概念的 C++20 编译器):
class CustomSerializer
{
static T Serialize(S);
static S Deserialize(T);
};
这里的思路是Serialize
的输入类型必须等于Deserialize
的输出类型,反之亦然
这可能吗?如果是,怎么做?
我尝试查看 std::invoke_result_t
,但您需要提供参数类型。但是Deserialize的参数类型是Serialize的调用结果,要得到Serialize的调用结果,...
我希望你看到这里的圆形图案,这让我想知道它是否可能。
对一组重载函数或模板函数的自省相当有限。但是在Serialize
和Deserialize
不会被重载的情况下,可以得到这些函数的return类型和参数类型,例如:
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))>);
在我看来,这个解决方案是不够的,因为这个概念会因为错误的原因而失败(当 Serialize
或 Deserialize
是模板和/或重载时)。
缓解措施可能是使用特征,因此当特征的类型提供您的库中未考虑的功能时,用户可以专门化该特征:
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.
简单的解决方案 - 检查函数指针是否相互依赖
这实际上通过模式匹配非常简单。我们可以编写一个 constexpr
函数,我将其称为 checkInverse
,如果类型反转,则 return 为真,否则为假:
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;
}
因为第一种情况更特殊,如果满足则函数将 return 为真,否则为 return 假。
然后我们可以使用它来检查 class 的 Serialize
和 Deserialize
方法是否相互匹配:
template<class T>
constexpr bool isValidPolicy() {
return checkInverse(T::Serialize, T::Deserialize);
}
如果我们不确定 class 有 Serialize
和 Deserialize
方法怎么办?
我们可以展开 isValidPolicy
来使用 SFINAE 检查。现在,只有 return 如果这些方法存在并且它们满足类型相互依赖性,它才会为真。
如果我调用 isValidPolicy<Type>(0)
,那么它将尝试使用 int
重载。如果 Serialize
和 Deserialize
不存在,它将回退到 long
重载和 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;
}
此解决方案的缺点是什么?
从表面上看,这似乎是一个很好的解决方案,尽管它确实存在一些问题。如果 Serialize
和 Deserialize
是模板化的,它将无法转换为函数指针。
此外,未来的用户可能希望编写 Deserialize
方法,使 return 对象可以转换为序列化类型。这对于直接将对象构造成向量而不进行复制非常有用,可以提高效率。此方法不允许 Deserialize
以这种方式编写。
高级解决方案 - 检查特定类型是否存在 Serialize
,以及 Deserialize
编辑的值 return 是否可以转换为该类型
这个解决方案更通用,最终也更有用。它使 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 {};
}
检查类型是否表示有效的序列化程序
我们可以使用上面的转换检查器来做到这一点。这将检查它的给定类型,该类型必须作为参数传递给模板。结果作为静态布尔常量给出。
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;
};
惰性反序列化器示例
我之前提到过,可以依靠覆盖转换运算符来进行序列化/反序列化。这是一个非常强大的工具,我们可以用它来编写惰性序列化器和反序列化器。例如,如果序列化表示是 char
的 std::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]};
}
};