Обработка стертых данных во время выполнения — как не изобретать велосипед?

Я работаю над кодом, который получает данные, которые выглядят так:

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP };
struct buffer {
    data_type element_type;
    size_t    size; // in elements of element_type, not bytes
    void*     data;
}

(это упрощенно, на самом деле в этой структуре гораздо больше типов, больше полей и т. д.)

Теперь я обнаружил, что пишу кучу служебного кода для «преобразования» значений перечисления в фактические типы и наоборот во время компиляции. Затем я понимаю, что мне нужно сделать кое-что из того, что мне нужно сделать то же самое во время выполнения, и с переменным количеством буферов... так что теперь, в дополнение к поиску значений на основе типовых признаков и перечислению- Поиск типов на основе параметров шаблона — я пишу код, который ищет std::type_infos. Это своего рода беспорядок.

Но на самом деле я не должен этого делать. Это повторяется, и я абсолютно уверен, что заново изобретаю велосипед - реализую то, что уже было написано много раз: компиляторы, СУБД, парсеры файлов данных, библиотеки сериализации и так далее.

Что я могу сделать, чтобы свести к минимуму потраченные впустую усилия в этом начинании?

Примечания:

  • Я получаю эти буферы во время выполнения и не могу просто отменить удаление типа во время компиляции (например, используя type_traits).
  • Я не могу изменить API. Вернее, я мог изменить в своем коде все, что хотел, но я все равно получаю данные в этом макете в памяти.
  • Я не просто беру такие буферы на вход, мне также нужно производить их на выходе.
  • Иногда мне нужно обрабатывать много разных буферов одновременно - даже переменное их количество (например, foo(buffer* buffers, int num_buffers);.
  • Решения C++11 предпочтительнее, чем более новые стандартные версии.
  • На самом деле я часто использую gsl, так что вы можете использовать его в своих ответах, если хотите. Что касается Boost, от него может быть политически сложно зависеть, но для целей вопроса StackOverflow, я думаю, это нормально.

person einpoklum    schedule 31.10.2018    source источник
comment
Если вы можете использовать С++ 17, вы можете посмотреть std:: variant   -  person Chemistree    schedule 31.10.2018
comment
Да, я хотел указать, что это можно свести к варианту векторов. Если только я что-то не упустил.   -  person StoryTeller - Unslander Monica    schedule 31.10.2018
comment
@Chemistree: это не помогает, потому что я не могу изменить API (см. редактирование).   -  person einpoklum    schedule 31.10.2018
comment
@StoryTeller: см. редактирование.   -  person einpoklum    schedule 31.10.2018
comment
Возможно, мой старый вопрос, а не о вариантах: Идиома для моделирования параметры числового шаблона во время выполнения?   -  person einpoklum    schedule 07.11.2018


Ответы (4)


Цель здесь должна заключаться в том, чтобы как можно быстрее вернуться к системе типов C++. Для этого должна быть одна центральная функция, которая переключается на основе (времени выполнения) data_type, а затем передает каждый случай версии шаблона (времени компиляции).

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

template<typename T>
struct TypedBuffer
{
  TypedBuffer(void* data, size_t elementCount) { /* ... */ }
  // ...
};

template<typename T>
void handleBufferTyped(void* data, size_t elementCount)
{
  TypedBuffer<T> buf(data, elementCount);
  // Do whatever you want - you're back in the type system.
}

void handleBuffer(buffer buf)
{
  switch (buf.element_type)
  {
  case INT16:     handleBufferTyped<int16_t>(buf.data, buf.size); break;
  case INT32:     handleBufferTyped<int32_t>(buf.data, buf.size); break;
  case UINT64:    handleBufferTyped<uint64_t>(buf.data, buf.size); break;
  case FLOAT:     handleBufferTyped<float>(buf.data, buf.size); break;
  case TIMESTAMP: handleBufferTyped<std::time_t>(buf.data, buf.size); break;
  }
}

При необходимости вы также можете наследовать TypedBuffer от базового класса без шаблона, чтобы вы могли вернуться из handleBuffer полиморфно, но это смешивает множество парадигм и, вероятно, не нужно.

person Max Langhof    schedule 31.10.2018
comment
1. Я спрашивал о том, как не писать такого рода вещи... 2. Мне также нужно перевести обратно. 3. Фактические буферы более сложны, и одного переключателя здесь недостаточно. 4. Это работает для одного буфера. Но у меня есть несколько функций, которые занимают несколько буферов, а если этого недостаточно - функции, которые занимают переменное количество буферов: void foo(buffer* buffers, int num_buffers);. - person einpoklum; 31.10.2018
comment
@einpoklum 1. Для возврата к системе типов требуются именно такие вещи. 2. С учетом того, что вы просили, тривиально, чтобы у каждого TypedBuffer была функция преобразования в buffer. Мы не можем дать вам хороший код/абстракции для этого, если мы не знаем, что между ними отличается/одинаково. 3. Мы не можем дать вам код для неспецифического, это сложнее. 4. Переберите буферы и обработайте каждый из них, как указано выше. Используйте полиморфию (как указано в ответе), если вам нужно (например, для хранения результирующих типизированных буферов в том же контейнере). - person Max Langhof; 31.10.2018
comment
@einpoklum В более общем плане: если ваш API полностью выходит за рамки системы типов С++ (как вы показываете), то единственный способ вернуться в систему типов С++ - это каким-то образом преодолеть этот пробел в каждой функции API. Способ сделать это с минимальными усилиями зависит от того, как выглядит ваш API. Например, у вас может быть где-то отображение числа типа ‹-› и цикл по нему или множество других вещей. Было бы намного легче обсуждать это, если бы в вопросе были репрезентативные примеры. В противном случае это, вероятно, слишком широко. - person Max Langhof; 31.10.2018

как не изобретать велосипед?

Просто используйте std::variant вместе с конверсиями туда и обратно. Он находится в стандартной библиотеке по какой-то причине.

Если говорить об изобретении велосипеда, то посещение — это простейший общий механизм для обработки данных со стертым типом.

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP, size };

template<data_type d>
struct data
{
    using type = void;
};
template<>
struct data<INT16>
{
    using type = int16_t;
};
// and so on

template<data_type d>
using data_t = typename data<d>::type;


template<typename F, typename T>
void indirect(void* f, void* t, int n)
{
    (*(F*)f)((T*)t, n);
}

template<typename F, size_t... Is>
void visit_(F&& f, buffer* bufs, int n, std::index_sequence<Is...>)
{
    using rF = typename std::remove_reference<F>::type;
    using f_t = void(*)(void*, void*, int);
    static constexpr f_t fs[] = {indirect<rF, data_t<data_type(Is)>>...};
    for(int i = 0; i < n; i++)
        fs[bufs[i].element_type](&f, bufs[i].data, bufs[i].size);
}

template<typename F>
void visit(F&& f, buffer* bufs, int n)
{
    visit_(std::forward<F>(f), bufs, n, std::make_index_sequence<data_type::size>{});
}

std::index_sequence и друзья могут быть относительно легко реализованы на C++11. Использовать как

struct printer
{
    template<typename T>
    void operator()(T* t, int n)
    {
        for(int i = 0; i < n; i++)
            std::cout << t[i] << ' ';
        std::cout << '\n';
    }
};

void foo()
{
    visit(printer{}, nullptr, 0);
}
person Passer By    schedule 31.10.2018

Кажется, для этого используются type_traits (https://en.cppreference.com/w/cpp/types).

По сути, вы определяете шаблонную структуру, по умолчанию она пуста, и вы специфицируете ее для каждого перечисления, которое у вас есть. Затем в своем коде вы используете MyTypeTraits<MyEnumValue>::type, чтобы получить тип, связанный с нужным перечислением.

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

person Matthieu Brucher    schedule 31.10.2018
comment
Чтобы использовать черты типа, вам нужно знать значение перечисления во время компиляции. Я знаю это только во время бега. - person einpoklum; 31.10.2018

Используйте boost::variant и gsl::span.

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP };
struct buffer {
  data_type element_type;
  size_t    size; // in elements of element_type, not bytes
  void*     data;
};

template<class...Ts>
using var_span = boost::variant< gsl::span< Ts > ... >;

using buffer_span = var_span< std::int16_t, std::int32_t, std::uint64_t, float, ??? >;

buffer_span to_span( buffer buff ) {
  switch (buff.element_type) {
    case INT16: return gsl::span<std::int16_t>( (std::int16_t*)buff.data, buff.size );
    // etc
  }
}

Теперь вы можете

auto span = to_span( buff );

а затем посетите диапазон для безопасного доступа к буферу данных.

Запись посетителей менее болезненна в c+ +14 из-за [](auto&&) лямбда-выражений, но выполнимо в С++11.

Написание template<class...Fs> struct overloaded также может облегчить запись посетителей. Существует множество реализаций.

Если вы не можете использовать boost, вы можете преобразовать to_span в visit_span и заставить его принять посетителя.

Если вы не можете использовать gsl, написание собственного span тривиально.

visit_span( buff, overload(
  [](span<int16_t> span) { /* code */ },
  [](span<int32_t> span) { /* code */ },
  // ...
 ));

or

 struct do_foo {
   template<class T>
   void operator()(span<T> span) { /* code */ }
 };
 visit_span( buff, do_foo{captures} );
person Yakk - Adam Nevraumont    schedule 31.10.2018