Решения для динамической отправки на несвязанные типы

Я исследую возможные реализации динамической диспетчеризации несвязанных типов в современном C ++ (C ++ 11 / C ++ 14).

Под «динамической диспетчеризацией типов» я имею в виду случай, когда во время выполнения нам нужно выбрать тип из списка по его интегральному индексу и что-то с ним сделать (вызвать статический метод, использовать черту типа и т. Д.).

Например, рассмотрим поток сериализованных данных: существует несколько видов значений данных, которые сериализуются / десериализуются по-разному; есть несколько кодеков, которые делают сериализацию / десериализацию; и наш код читает маркер типа из потока, а затем решает, какой кодек он должен вызывать для считывания полного значения.

Меня интересует случай, когда существует много операций, которые могут быть вызваны для типов (несколько статических методов, черты типов …), и где может быть другое отображение из логических типов в классы C ++, а не только 1: 1 (в Например, с сериализацией это означает, что может быть несколько типов данных, которые все сериализуются одним и тем же кодеком).

Я также хотел бы избежать повторения кода вручную и сделать его более легким в обслуживании и менее подверженным ошибкам. Производительность также очень важна.

В настоящее время я вижу эти возможные реализации, я что-то упустил? Можно ли сделать это лучше?

  1. Вручную напишите столько функций с помощью switch-case, сколько существует возможных вызовов операций над типами.

    size_t serialize(const Any & any, char * data)
    {
    switch (any.type) {
    case Any::Type::INTEGER:
    return IntegerCodec::serialize(any.value, data);
    ...
    }
    }
    Any deserialize(const char * data, size_t size)
    {
    Any::Type type = deserialize_type(data, size);
    switch (type) {
    case Any::Type::INTEGER:
    return IntegerCodec::deserialize(data, size);
    ...
    }
    }
    bool is_trivially_serializable(const Any & any)
    {
    switch (any.type) {
    case Any::Type::INTEGER:
    return traits::is_trivially_serializable<IntegerCodec>::value;
    ...
    }
    }
    

Pros: это просто и понятно; Компилятор может встроить отправленные методы.

Cons: это требует много ручного повторения (или генерации кода с помощью внешнего инструмента).

  1. Создайте таблицу диспетчеризации, как это

    class AnyDispatcher
    {
    public:
    virtual size_t serialize(const Any & any, char * data) const = 0;
    virtual Any deserialize(const char * data, size_t size) const = 0;
    virtual bool is_trivially_serializable() const = 0;
    ...
    };
    class AnyIntegerDispatcher: public AnyDispatcher
    {
    public:
    size_t serialize(const Any & any, char * data) const override
    {
    return IntegerCodec::serialize(any, data);
    }
    Any deserialize(const char * data, size_t size) const override
    {
    return IntegerCodec::deserialize(data, size);
    }
    bool is_trivially_serializable() const
    {
    return traits::is_trivially_serializable<IntegerCodec>::value;
    }
    ...
    };
    ...
    
    // global constant
    std::array<AnyDispatcher *, N> dispatch_table = { new AnyIntegerDispatcher(), ... };
    
    size_t serialize(const Any & any, char * data)
    {
    return dispatch_table[any.type]->serialize(any, data);
    }
    Any deserialize(const char * data, size_t size)
    {
    return dispatch_table[any.type]->deserialize(data, size);
    }
    bool is_trivially_serializable(const Any & any)
    {
    return dispatch_table[any.type]->is_trivially_serializable();
    }
    

Prosэто немного более гибко — нужно написать класс диспетчера для каждого отправляемого типа, но затем можно объединить их в разных таблицах диспетчеризации.

Cons: это требует написания большого количества диспетчерского кода. И есть некоторые издержки из-за виртуальной диспетчеризации и невозможности встроить методы кодека на сайт вызывающей стороны.

  1. Используйте шаблонную функцию диспетчеризации

    template <typename F, typename... Args>
    auto dispatch(Any::Type type, F f, Args && ...args)
    {
    switch (type) {
    case Any::Type::INTEGER:
    return f(IntegerCodec(), std::forward<Args>(args)...);
    ...
    }
    }
    
    size_t serialize(const Any & any, char * data)
    {
    return dispatch(
    any.type,
    [] (const auto codec, const Any & any, char * data) {
    return std::decay_t<decltype(codec)>::serialize(any, data);
    },
    any,
    data
    );
    }
    bool is_trivially_serializable(const Any & any)
    {
    return dispatch(
    any.type,
    [] (const auto codec) {
    return traits::is_trivially_serializable<std::decay_t<decltype(codec)>>::value;
    }
    );
    }
    

Pros: для этого требуется только одна функция диспетчеризации переключателя и немного кода в каждом вызове операции (по крайней мере, вручную). И компилятор может включить то, что он считает подходящим.

Cons: это более сложно, требует C ++ 14 (чтобы быть таким чистым и компактным) и полагается на способность компилятора оптимизировать удаление неиспользуемого экземпляра кодека (который используется только для выбора правильной перегрузки для кодека).

  1. Когда для одного набора логических типов может быть несколько отображений на классы реализации (в этом примере кодеки), может быть лучше обобщить решение № 3 и написать полностью универсальную функцию диспетчеризации, которая получает отображение времени компиляции между значениями типов и вызываемыми типами , Что-то вроде этого:

    template <typename Mapping, typename F, typename... Args>
    auto dispatch(Any::Type type, F f, Args && ...args)
    {
    switch (type) {
    case Any::Type::INTEGER:
    return f(mpl::map_find<Mapping, Any::Type::INTEGER>(), std::forward<Args>(args)...);
    ...
    }
    }
    

Я опираюсь на решение № 3 (или № 4). Но я удивляюсь — можно ли избежать написания вручную dispatch функционировать? Я имею ввиду его распределительный шкаф. Этот случай переключения полностью получен из отображения времени компиляции между значениями типа и типами — есть ли способ обработать его генерацию для компилятора?

8

Решение

Вот решение где-то между вашими # 3 и # 4. Может быть, это вдохновляет, но не уверен, что это действительно полезно.

Вместо того, чтобы использовать базовый класс интерфейса и виртуальную диспетчеризацию, вы можете просто поместить свой кодек код в некоторые несвязанные структуры признаков:

struct AnyFooCodec
{
static size_t serialize(const Any&, char*)
{
// ...
}

static Any deserialize(const char*, size_t)
{
// ...
}

static bool is_trivially_serializable()
{
// ...
}
};

struct AnyBarCodec
{
static size_t serialize(const Any&, char*)
{
// ...
}

static Any deserialize(const char*, size_t)
{
// ...
}

static bool is_trivially_serializable()
{
// ...
}
};

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

typedef std::tuple<AnyFooCodec, AnyBarCodec> DispatchTable;

Теперь мы можем написать универсальную диспетчерскую функцию, которая передает черту n-го типа данному функтору:

template <size_t N>
struct DispatchHelper
{
template <class F, class... Args>
static auto dispatch(size_t type, F f, Args&&... args)
{
if (N == type)
return f(typename std::tuple_element<N, DispatchTable>::type(), std::forward<Args>(args)...);
return DispatchHelper<N + 1>::dispatch(type, f, std::forward<Args>(args)...);
}
};

template <>
struct DispatchHelper<std::tuple_size<DispatchTable>::value>
{
template <class F, class... Args>
static auto dispatch(size_t type, F f, Args&&... args)
{
// TODO: error handling (type index out of bounds)
return decltype(DispatchHelper<0>::dispatch(type, f, args...)){};
}
};

template <class F, class... Args>
auto dispatch(size_t type, F f, Args&&... args)
{
return DispatchHelper<0>::dispatch(type, f, std::forward<Args>(args)...);
}

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

Живой пример: http://coliru.stacked-crooked.com/a/1c597883896006c4

0

Другие решения

Диспетчеризация тегов, когда вы передаете тип для выбора перегрузки, эффективна. std библиотеки обычно используют его для алгоритмов на итераторах, поэтому разные категории итераторов получают разные реализации.

Когда у меня есть список идентификаторов типов, я гарантирую, что они смежные, и пишу таблицу переходов.

Это массив указателей на функции, которые выполняют поставленную задачу.

Вы можете автоматизировать написание этого на C ++ 11 или лучше; Я называю это магический переключатель, поскольку он действует как переключатель времени выполнения, и он вызывает функцию со значением времени компиляции, основанным на времени выполнения. Я делаю функции с лямбдами и расширяю пакет параметров внутри них, чтобы их тела различались. Затем они отправляют объекту переданной функции.

Напишите это, и тогда вы сможете переместить свой код сериализации / десериализации в код типа «safe type». Используйте черты для сопоставления индексов времени компиляции с типами тегов и / или отправки на основе индекса в перегруженную функцию.

Вот магический переключатель C ++ 14:

template<std::size_t I>using index=std::integral_constant<std::size_t, I>;

template<class F, std::size_t...Is>
auto magic_switch( std::size_t I, F&& f, std::index_sequence<Is...> ) {
auto* pf = std::addressof(f);
using PF = decltype(pf);
using R = decltype( (*pf)( index<0>{} ) );
using table_entry = R(*)( PF );

static const table_entry table[] = {
[](PF pf)->R {
return (*pf)( index<Is>{} );
}...
};

return table[I](pf);
}

template<std::size_t N, class F>
auto magic_switch( std::size_t I, F&& f ) {
return magic_switch( I, std::forward<F>(f), std::make_index_sequence<N>{} );
}

Использование выглядит так:

std::size_t r = magic_switch<100>( argc, [](auto I){
return sizeof( char[I+1] ); // I is a compile-time size_t equal to argc
});
std::cout << r << "\n";

живой пример.

Если вы можете зарегистрировать тип enum для ввода типа map во время компиляции (с помощью черт типа или чего-либо еще), вы можете совершить круговое переключение с помощью магического переключателя, чтобы превратить значение enum времени выполнения в тег типа времени компиляции.

template<class T> struct tag_t {using type=T;};

тогда вы можете написать свою сериализацию / десериализацию следующим образом:

template<class T>
void serialize( serialize_target t, void const* pdata, tag_t<T> ) {
serialize( t, static_cast<T const*>(pdata) );
}
template<class T>
void deserialize( deserialize_source s, void* pdata, tag_t<T> ) {
deserialize( s, static_cast<T*>(pdata) );
}

Если у нас есть enum DataTypeпишем черты:

enum DataType {
Integer,
Real,
VectorOfData,
DataTypeCount, // last
};

template<DataType> struct enum_to_type {};

template<DataType::Integer> struct enum_to_type:tag_t<int> {};
// etc

void serialize( serialize_target t, Any const& any ) {
magic_switch<DataType::DataTypeCount>(
any.type_index,
[&](auto type_index) {
serialize( t, any.pdata, enum_to_type<type_index>{} );
}
};
}

все тяжелые работы теперь выполняются enum_to_type черты класса специализации, DataType enum и перегрузки формы:

void serialize( serialize_target t, int const* pdata );

которые типа безопасны.

Обратите внимание, что ваш any на самом деле не any, а скорее variant, Он содержит ограниченный список типов, а не что-нибудь.

это magic_switch в конечном итоге используется для переопределения std::visit функция, которая также дает вам безопасный тип доступа к типу, хранящемуся в variant,

Если вы хотите, чтобы он содержал что-нибудь, Вы должны определить, какие операции вы хотите поддерживать, написать для него код стирания типа, который выполняется при сохранении в any, храните удаленные операции вместе с данными, и Боб — ваш дядя.

4

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector