LINUX.ORG.RU

С++ ООП дизайн для хранилища семплов

 , , ,


0

1

Приветствую коллеги,

Дорабатываю имеющуюся C++ обертку для порт аудио.

Не могу понять как мне организовать хранение семплов, простых числовых значений типа int разной длины или float. Вопрос по ООП дизайну.

К сожалению имеющая кодовая база построена так разные типы семплов хранятся в разных типах шаблонных классов

MyBuffer<INT_16> 
MyBuffer<FLOAT_32> 

Хранилище типизировано и везде в коде который использует класс MyBuffer стоят switch/if по типу контейнера. Что мне кажется не очень правильно, при добавлении нового типа семплов везде придется ставить дополнительные if блоки.

В каждом таком хранилище будет только один тип значений. Наверное будет правильно сделать MyBuffer не шаблонным а обычным классом. Я могу использовать простой union для хранения разных типов данных в одном объекте и хранить информацию о том какой в целом тип содержит мой контейнер. ( Есть и std::variant из С++17 но мне кажется тут можно обойтись и простым union) Т.е внутри моего класса будет

std::vector<myUnion> dataStorage;

Также можно хранить данные в сыром виде (массив unsigned char) и тоже хранить информацию о том какой тип данных хранится реально внутри контейнера.

Или же можно создать базовый тип Sample и наследников семплы разного формата Sample16, Sample32Float например. И в таком случае хранить коллекцию указателей на этот базовйый тип

std::vector<*Sample>

Хотелось бы еще и данные как то в одном виде отдавать из этого контейнера и избежать всех if/switch по типу.

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

type? GetSample(int ch, int frame) 

либо надо иметь разные методы, что привежет к тем же if/switch в коде который использует класс MyBuffer()

int GetIntSample(int ch, int frame);
float GetFloatSample(int ch, int frame); 

либо просто отдавать сырые данные в массиве char

void GetSample(int ch, int frame, char* outBuffer);

Может быть действиельно обойтись простым хранением unsigned char, и отдавать эти данные unisigned char для всех? Например при отправке этих данных по сети они все в любом случае в массив байт преобразуются.

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

Буду благодарен за идеи! Спасибо!

Хранилище типизировано и везде в коде который использует класс MyBuffer стоят switch/if по типу контейнера. Что мне кажется не очень правильно, при добавлении нового типа семплов везде придется ставить дополнительные if блоки.

Зависит от того, что там делается. Добавление иерархии может ничего не улучшить, а только усложнить. Без примеров кода что-то советовать будет трудно. Возможно, что MyBuffer заведён по какой-то причине.

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

Там простой if для получения корректного типа MyBuffer и получения данных с помощью GetSample() Метод выглядит так, возвращаемое значение разное.

virtual T GetSample(int ch, int frame);

т.е никакой хитрой логики я не вижу.

salvequick ()

В учебниках это всё и вправду разжёвано.

Хранилище типизировано и везде в коде который использует класс MyBuffer стоят switch/if по типу контейнера.

А почему они там стоят? Тебе надо решить одно из двух - либо у тебя класс и вся работа с ним параметризуется шаблонным типом, и тогда у тебя будут разные шаблонные методы в зависимости от типа объекта, либо тебе нужны разные реализации, имеющие общий публичный интерфейс, независящий от внутреннего представления данных - тогда наследование.

обойтись простым хранением unsigned char, и отдавать эти данные unisigned char для всех? Например при отправке этих данных по сети они все в любом случае в массив байт преобразуются.

Достаточно сделать общий интерфейс, выдающий данные в таком виде.

LamerOk ★★★★★ ()

Сделай приватными полями указатели на векторы семплов каждого формата, который планируешь поддерживать.

anonymous ()
Ответ на: комментарий от xaizek

А сами функции шаблонные или как они компилируются, если возвращаемые типы разные?

Да это шаблонные функции методы этого класса MyBuffer у них возвращаемое значение typename T тип. Поэтому они и получаются разные.

salvequick ()
Последнее исправление: salvequick (всего исправлений: 1)
Ответ на: комментарий от anonymous

Сделай приватными полями указатели на векторы семплов каждого формата, который планируешь поддерживать.

Это получается такой брутфорс и не решает проблему возвращаемых значений. Придется реализоваться GetIntSample, GetFloatSample и так для каждого типа.

salvequick ()
Последнее исправление: salvequick (всего исправлений: 1)
Ответ на: комментарий от LamerOk

В учебниках это всё и вправду разжёвано. Я бы с удовольствием почитал если бы порекомендовали что то. Без всякой иронии говорю, вижу сам что «плаваю» в вопросе.

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

Так вот проблема то в том, что не могу я принять решение.
Наверное надо либо остановиться на char либо хранимый тип должен быть полиморфным.

Достаточно сделать общий интерфейс, выдающий данные в таком виде.

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

salvequick ()
Последнее исправление: salvequick (всего исправлений: 1)
Ответ на: комментарий от salvequick

Так может просто сделать N специализаций этого класса: по одной на каждый из поддерживаемых типов? Вот ветвление и уйдёт. Если часть функций общая, то можно специализировать только те методы, в которых есть ветвление.

xaizek ★★★★★ ()
Последнее исправление: xaizek (всего исправлений: 1)
Ответ на: комментарий от xaizek

Так может просто сделать N специализаций этого класса: по одной на каждый из поддерживаемых типов?

Может я неправильно выразился. Я имел ввиду что тем классам кто использует MyBuffer<> приходится ветвиться. Внутри MyBuffer ветвлений нет. Вся кодовая база прониана ветвлениями по типу семпла. Например создаются два буфера с разыми типами данных и тому подобное.

Примерно так выглядит буфер:

template <typename T>
class MyBuffer
{
public:
   MyBuffer(int ch, int frame){ /// some init code }
   T GetSample(int ch, int frame);

private:
   vector<vector<T>> m_samples;
};

salvequick ()

Варианты:

  1. Перейти к одному типу данных в интерфейсе буфера, например float. Конвертировать при необходимости.
  2. Сделать класс, который использует MyBuffer, шаблонным от типа сэмпла. Ветвление перейдёт на этап создания этого класса и будет локализовано.
m0xf ()

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

Почему не сможет? Mybuffer это же отдельный тип, у него будет всего один метод getsample который возвращает сампл нужного типа. Чтобы не ветвиться сделай специализацию для этого типа самплов, или как вариант, сконвертируй к одному типу и работай с ним. Но тогда не понятно откуда в коде ветвление если все семплы обрабатываются одинаково.

По-моему ты не там смотришь, тебе надо смотреть в обработку семплов, а не в хранение.

anonymous ()

Может имеет смысл вообще отказаться от всего этого зоопарка форматов и поддерживать только один? Есть свой «внутренний» формат семплов, например вектор из double и вся обработка идёт только с этим форматом. При необходимости импорта/экспорта семплов, производится преобразование в требуемый формат. По-моему это гораздо логичнее и удобнее, хотя конечно зависит от решаемой задачи.

m0rph ★★★★★ ()

Одним из учебников, в «котором всё разжевано» есть так нелюбимая многими «Александресочка». Ака «Современное проектирование на С++». Теперь, если учитывать дату выхода книжечки и актуальный стандарт, проектирование будет выглядеть не очень то и современным, но всё же, если конкретнее, то в «Александресочке» предлагается почитать главу 10, «Шаблон Visitor» и, визможно главу 11 «Мультиметоды».

Мне же кажется что «плата за абстракции» выделки не стоит, и добавлять новый case в switch приходится не так уж часто, как правило на этапе «подёргиваний» и начального дизайна, потом, когда всё устаканилось - switch живёт себе и кушать не просит.

nikitos ★★ ()
Ответ на: комментарий от nikitos

Если проблема исключительно в switch, то можно использовать паттерн visitor. Другое дело, что в контексте решаемой задачи это может быть из пушки по воробьям.

m0rph ★★★★★ ()
Ответ на: комментарий от m0rph

Может имеет смысл вообще отказаться от всего этого зоопарка форматов и поддерживать только один?

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

salvequick ()
Ответ на: комментарий от nikitos

Одним из учебников, в «котором всё разжевано» есть так нелюбимая многими «Александресочка». Ака «Современное проектирование на С++».

Да именно эту книгу я и читал не далее как позавчера в попытках найти ответы на вопросы. И именно главу 11 «Мультиметоды»
Мне покаазалось, что это просто overengineering для моей задачи. Будет очень сложно объяснить коллегам и поверить самому что Алесандреску стиль лучше чем понятный switch. Ведь этот код потом спустя какое то время кто то должен читать и понимать.

salvequick ()

Наверное будет правильно сделать MyBuffer не шаблонным а обычным классом. Я могу использовать простой union для хранения разных типов данных в одном объекте и хранить информацию о том какой в целом тип содержит мой контейнер. ( Есть и std::variant из С++17 но мне кажется тут можно обойтись и простым union)

Используй виртуальные методы.

метод не сможет вернуть разные типы данных.

Тогда тип сэмпла оборачивай.

Может быть действиельно обойтись простым хранением unsigned char, и отдавать эти данные unisigned char для всех?

Тот же union, с теми же проблемами.

cloun1902 ()
struct iAlgo {
  virtual void here_is_ints(std::vector<int>) = 0;
  virtual void here_is_floats(std::vector<float>) = 0;
};

struct iMyBuffer {
  virtual void get_sample(int ch, int frame, iAlgo &a) = 0;
};

template <typename A1, typename A2>
struct Algo : iAlgo {
  void here_is_ints(std::vector<int> ints) override {
    A1::apply(ints);
  }
  void here_is_floats(std::vector<float> floats) override {
    A2::apply(floats);
  }
};

можно обмазать темплейтами там где нужно. например оператор комбинации разных Algo и т.д. задача на три копейки

anonymous ()

Может я неправильно выразился. Я имел ввиду что тем классам кто использует MyBuffer<> приходится ветвиться. Внутри MyBuffer ветвлений нет. Вся кодовая база прониана ветвлениями по типу семпла.

Так алгоритм работающий с MyBuffer сделай шаблонным. Если там какие-то собенности свои, то выноси их в TypeTraits с разными специализациями:

template <typename T>
struct Sample_traits;

template <>
struct Sample_traits<INT_16> {
   static anything(...) {}
   static constexpr unsigned frequency = 2048;
};

Может быть действиельно обойтись простым хранением unsigned char

Если так годится, то даже думать нечего, самое простое, char может что угодно алиасить.

pavlick ★★ ()
Ответ на: комментарий от salvequick

Еще одну штуку вспомнил, для скрещивания темплейтов с макросами, чтоб немного уменьшить количество боёлерплейта с switch, но сломать (возможно) голову сопровождающим:

https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

Примеры использования:

  1. https://github.com/colmap/colmap/blob/dev/src/base/camera_models.h см. макросы CAMERA_MODEL_DEFINITIONS + CAMERA_MODEL_CASES
  2. https://github.com/colmap/colmap/blob/dev/src/mvs/patch_match_cuda.cu см. макрос CASE_WINDOW_STEP
nikitos ★★ ()
Последнее исправление: nikitos (всего исправлений: 1)

Есть два решения, одно нормальное, другое из ООП.

Раз ты хочешь из ООП то нужно вспомнить про то что всё есть объект, про полиморфизьму и прочую хрень и забыть про буферы, которые по сути просто коллекции и начинать объектить понятие семпла. Я так понимаю, динамический полиморфизм тут необязателен, никакого наследования (чистый POD, без vtable, без алигнмента), просто куча классов типа int16sample, стандартный сет операций над семплами, перегрузку этого сета для каждого типа семпла и шлабоны стандартных операций работающие над шаблонным параметром с использованием операций из сетов.

Ну а нормальное - оставь как есть, потому как переписывание под ООП бесполезно. У тебя уже есть рабочий код и сильно сомнительно что в скором времени изобретут много новых форматов семплинга. На крайняк можно выделить пару-тройку таких, в которые безопасно перевести для обработки.

khrundel ★★★ ()

стоят switch/if по типу контейнера

Эпик фейл.

метод не сможет вернуть разные типы данных

И не надо, для этого шаблоны есть.

GetSample

Самое главное, тебе не нужен никакой getSample. У тебя есть объект - набор семплов. Сделай ему метод play или что ты там хочешь. А как оно внутри там будет работать никого не должно волновать.

no-such-file ★★★★★ ()
Ответ на: комментарий от no-such-file

Сделай ему метод play или что ты там хочешь.

Идея верная, но не до конца. Play - слишком высокоуровневая функция. У него же про обработку говорится. Нужно что-то типа умножения семпла или окна семплов на константу с сатурацией, нужны базовые математические операции над семплами, линейная интерполяция между семплами, нужен, наверняка, ресемплинг. Вообще можно заморочиться над non-destructive редактированием, типа над буфером создаётся источник семплов, над ним операции создающие другие ленивые источники семплов типа вставить/вырезать кусок, операция над семплами интервала ну и т.п.

khrundel ★★★ ()
Ответ на: комментарий от khrundel

У него же про обработку говорится. Нужно что-то типа умножения семпла или окна семплов на константу с сатурацией, нужны базовые математические операции над семплами, линейная интерполяция между семплами, нужен, наверняка, ресемплинг.

На самом деле все что требуется делать с семплами это стримить их в сокет, т.е просто получать массив char и записывать в файл, т.е опять же получать набор байт и записывать в их файл.

Думаю что разумно сделать общий доступ к данным в готовом виде для записи в сокет или файл. Видимо остановлюсь на чем то таком.

class IMyBuffer
{
public:
   // возможно можно обойтись и одним методом
   virtual void getBytesForStreaming(int len, char *buffer) = 0;
   virtual void getBytesForWritingToFile(int len, char *buffer) = 0;
}

template <typename T>
class MyBuffer : public IMyBuffer
{
public:
   MyBuffer(int channels, int capacity);
   
   // Фрейм это семплы со всех имеющихся каналов соответствующие 
   // определенной временной метке, отдельно семплами оперировать нет смысла 
   vector<T> GetFrame();
   T AddFrame(vector<T> samples);
   
   virtual void getBytesForStreaming(int len, char *buffer) override;
   virtual void getBytesForWritingToFile(int len, char *buffer) override;

private:
   vector<boost::circular_buffer<T>> m_samples;
};
salvequick ()
Ответ на: комментарий от salvequick

Тогда это не ООП, а просто хранилище байт. Ещё хуже чем топикстартер предлагал.

Смысл ООП в абстракциях. Если сделать буфер как источник семплов или, тем более, байт, то это просто байтовый буфер, который используется не-ООП программой. Если сделать 3 разных класса с общей функцией play, то это 3 не-ООП программы с неким общим интерфейсом. Чтоб получилось ООП надо находить абстракции из предметной области с инкапсулированными данными и операции над ними. В данном случае это семплы, потоки семплов, алгоритмы обработки, ну и и.п.

khrundel ★★★ ()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.