Обнаружение структуры взаимозависимых функций с SFINAE - PullRequest
8 голосов
/ 06 мая 2019

Я работаю над специальной библиотекой сериализации с настраиваемыми сериализаторами. Я хочу иметь возможность обнаруживать и применять концепцию Serializer в моей библиотеке с помощью SFINAE (у меня нет доступа к компилятору C ++ 20 с поддержкой концепций):

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

Идея заключается в том, что тип ввода Serialize должен быть равен типу вывода Deserialize, и наоборот

Возможно ли это? Если да, то как?

Я пытался изучить std::invoke_result_t, но тогда вам нужно указать типы аргументов. Но тип аргумента Deserialize является результатом вызова Serialize, и чтобы получить результат вызова Serialize, ...

Вы видите здесь круговую схему, я надеюсь, что заставляет меня задуматься, возможно ли это вообще.

Ответы [ 2 ]

3 голосов
/ 06 мая 2019

Простое решение - проверьте, являются ли указатели функций взаимозависимыми

Это действительно очень просто сделать с помощью сопоставления с образцом.Мы можем написать функцию 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.

Затем мы можем использовать это, чтобы проверить, совпадают ли методы Serialize и Deserialize класса друг с другом:

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

Что если мы не уверены, что класс имеет Serialize и Deserialize метод?

Мы можем расширить isValidPolicy, чтобы проверить это с помощью SFINAE.Теперь он вернет true, только если эти методы существуют, И они удовлетворяют взаимозависимости типа.

Если я позвоню isValidPolicy<Type>(0), он попытается использовать перегрузку int.Если Serialize и Deserialize не существует, это приведет к перегрузке 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; 
}

Каковы минусы этого решения?

На первый взгляд это кажется хорошим решением, хотя у него есть несколько проблем.Если шаблон Serialize и Deserialize настроен, он не сможет выполнить преобразование в указатель на функцию.

Кроме того, будущие пользователи могут захотеть написать 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 {}; 
}

Проверка, представляет ли тип действительный сериализатор

Мы можем использовать вышеуказанную проверку преобразования для этого.Это проверит его для данного типа, который должен быть передан в качестве параметра в шаблон.Результат дается в виде статической константы bool.

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

Пример ленивого десериализатора

Я упоминал ранее, что можно полагаться на наложение оператора преобразования для сериализации / десериализации.Это чрезвычайно мощный инструмент, который мы можем использовать для написания ленивых сериализаторов и десериализаторов.Например, если сериализованное представление представляет собой std::array из 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; 
    }
};

Как только мы получим это, написание политики 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]}; 
    }
};
1 голос
/ 06 мая 2019

Самоанализ набора перегруженных функций или шаблонной функции довольно ограничен. Но в случае, если Serialize и Deserialize не будут перегружены, можно получить тип возвращаемого значения и тип аргумента этих функций, например:

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.
...