LINUX.ORG.RU

Вопрос по дизайну

 , ,


0

4

Здравствуйте. Есть вопрос, как правильно задизайнить.

Есть простенький DSV-writer. Хочется путём лёгкой кастомизации превращаться его в CSV, TSV, etc. writer.

И тут у меня появились две соперничающие идеи, как эту кастомизацию сделать.

1.


template <typename Delimiter,
          typename LineTerminator,
          typename Quote,
          typename Comment>
struct Dialect
{
    using DelimiterType = Delimiter;
    using LineTerminatorType = LineTerminator;
    using QuoteType = Quote;
    using CommentType = Comment;

    Dialect() = default;

    Dialect(Delimiter delimiter1, LineTerminator lineTerminator1, Quote quote1, Comment comment1) :
            delimiter(delimiter1), lineTerminator(lineTerminator1), quote(quote1), comment(comment1)
    {}


    Delimiter delimiter;
    LineTerminator lineTerminator;
    Quote quote;
    Comment comment;
};

class Writer
{
    Dialect d;
public:
    Writer(const Dialect& dialect) : d(dialect) {}
    //...
};

const Dialect<char,char,char,char> CSV_Dialect = Dialect<char,char,char,char>(',', '\n', '"', '#');
const Dialect<char,char,char,char> TSV_Dialect = Dialect<char,char,char,char>('\t', '\n', '"', '#');

//.....

int main()
{
    Writer writer(CSV_Dialect);
}

Плюсы подхода - очень просто создать новый диалект. Просто создаём новую переменную типа диалект, задаём нужные нам типы внутри, пихаем туда любые значения, а потом скармливаем это Writer.

2.


template <typename Delimiter,
          typename LineTerminator,
          typename Quote,
          typename Comment>
struct Dialect
{
    using DelimiterType = Delimiter;
    using LineTerminatorType = LineTerminator;
    using QuoteType = Quote;
    using CommentType = Comment;

    Dialect() = default;

    Dialect(Delimiter delimiter1, LineTerminator lineTerminator1, Quote quote1, Comment comment1) :
            delimiter(delimiter1), lineTerminator(lineTerminator1), quote(quote1), comment(comment1)
    {}


    Delimiter delimiter;
    LineTerminator lineTerminator;
    Quote quote;
    Comment comment;
};

template <typename Delimiter,
          typename LineTerminator,
          typename Quote,
          typename Comment>
struct CSV_Dialect : public Dialect<Delimiter,LineTerminator,Quote,Comment>
{
    CSV_Dialect() : Dialect<Delimiter,LineTerminator,Quote,Comment>(',', '\n', '"', '#') {}
};

template <typename Delimiter,
          typename LineTerminator,
          typename Quote,
          typename Comment>
struct TSV_Dialect : public Dialect<Delimiter,LineTerminator,Quote,Comment>
{
    TSV_Dialect() : Dialect<Delimiter,LineTerminator,Quote,Comment>('\t', '\n', '"', '#') {}
};

template <typename Dialect>
class Writer
{
    Dialect d;
    //...
};


//.....

int main()
{
    Writer<CSV_Dialect> writer;
}

Тут проблема в том, что создать новый диалект сложнее.

Если у кого-то есть ещё идеи, как сделать лучше - милости прошу к обсуждению.

eao197

Тут проблема в том, что создать новый диалект сложнее

А плюсов никаких.

Туеву хучу typename лучше запихать в трайт.

no-such-file ★★★★★ ()

У тебя планируется ситуация, когда вид writer'а придётся выбирать в рантайме? Например ./prog --style=csv? Тогда лучше

int main()
{
    Writer writer(CSV_Dialect);
}

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

Ну конечно такой вариант возможен. Я ж не знаю, как там пользователь захочет юзать мой Writer

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

Но с этим подходом возникает интересная ситуация - в каком виде предоставлять пользователю уже готовые Dialect?

В случае с шаблонной передачей диалекта понятно - наследуем новый тип и вперёд.

А вот в случае с передачей в конструкторе - я должен заранее создать переменные диалекты. Нормально ли это? То есть переменные могут просто так висеть

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

Создай оба варианта. По фидбеку поймешь, какой лучше зашел.

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

А вот в случае с передачей в конструкторе - я должен заранее создать переменные диалекты. Нормально ли это?

Нормально. Хедеры для этого и созданы.

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

Ок, понял.

Есть какие-нибудь ещё замечания к такому дизайну?

zamazan4ik ★★ ()

Ещё интересный кейс в том, что например у нас есть диалект CSV. теперь я хочу специализировать CSV_Excel. Тут уже наследоваться надо от CSV, что говорит ещё раз в пользу иерархии таких вот диалектов.

zamazan4ik ★★ ()

Если честно, то, боюсь, я не понял, в чем именно проблема выбора между двумя вариантами оформления Dialect-а.

А вот что понял, так это то, что по сути у вас есть проблема того, как представить Writer. Либо Writer у вас будет обычным классом, который принимает только экземпляры класса Dialect (при этом у вас нет возможности сделать наследника Dialect с дополнительными атрибутами внутри и скормить его Writer-у, т.к. при копировании в конструкторе Writer-а произойдет срезка).

Либо же Writer у вас будет шаблоном и вы сможете передать в конструктор любой экземпляр Dialect-а, который похож на Dialect. Это может быть как наследник класса Dialect с дополнительными полями внутри, так и вообще не связанный с Dialect-ом класс, который мимикрирует под Dialect (утиная типизация по C++ному).

При этом у вас еще есть вот такое важное отличие между двумя этими вариантами. В первом случае у вас всегда один тип Writer-а, вне зависимости от того, пишет ли он в DSV, в CSV или в TSV. Во втором случае у вас на уровне типов Writer<DSV_Dialect> будет отличаться от Writer<CSV_Dialect>.

Будет ли это иметь принципиальное значение и, если будет, то с каким знаком, зависит от специфики вашей задачи. Первый подход позволяет вам написать что-то вроде:

void dump_document(const Document & what, Writer & to) {...}
и вы в run-time можете подсовывать в dump_document разные типы Writer-ов. Второй подход заставит вас делать dump_document шаблоном, например:
template<typename D>
void dump_document(const Document & what, Writer<D> & to) {...}
Причем, в зависимости от условий, вам может быть нужно либо одно, либо другое.

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

template<char Delimiter,
          char LineTerminator,
          char Quote,
          char Comment>
class SpecificDialect : public Dialect<char, char, char, char> {
public :
  SpecificDialect() : Dialect{Delimiter, LineTerminator, Quote, Comment> {}
};
...
using CSV_Dialect = SpecificDialect<',', '\n', '"', '#'>;
using TSV_Dialect = SpecificDialect<'\t', '\n', '"', '#'>;

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

Да, Вы совершенно верно подметили - вся соль как раз таки и заключается в представлении Writer (а от него и всё дальше пляшет).

Теперь надо бы пройтись по вариантам, которые Вы предложили:

Либо Writer у вас будет обычным классом, который принимает только экземпляры класса Dialect (при этом у вас нет возможности сделать наследника Dialect с дополнительными атрибутами внутри и скормить его Writer-у, т.к. при копировании в конструкторе Writer-а произойдет срезка).

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

Со вторым вариантом - вроде бы и так всё понятно.

Как выбрать - вопрос конечно интересный. По моему скромному мнению, первый вариант удобнее использовать, нежели второй.

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

Оно прекрасно работало, если бы не одно но. Параметром шаблона может быть не только trivial-type, а там уже через шаблоны таким образом значение не удастся передать.

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

Если можно избежать динамического полиморфизма, то почему бы не попытаться не сделать это?

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

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

Если можно избежать динамического полиморфизма, то почему бы не

попытаться не сделать это? Вопрос в том, зачем его вообще избегать?

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

тебе нужен интерфейс Диалект. И классы, его реализующие, должны сами реализовывать запись по шаблону. Ведь для CSV запись строки - это конкатенация стрингов, а в Excel - уже будет использоваться доп. либа какая-нибудь. Не получится это все реализовывать на уровне Writer-а.

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

Лучше всё динамическим полиморфизмом из ведра поливать. Гибко же!

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

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

Любознательному читателю намёк: вся соль в том, насколько компилятор способен оптимизировать код возле виртуального вызова и внутри него.

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

я думаю, что он как бы думает, что: 1) Виртуальные вызовы дешёвые (нет, это не совсем так. Ну мы все это знаем) 2) Надежда на девиртуализацию (но она срабатывает далеко не всегда)

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

угу.

1) зачем лезть в кишки процессора - это ж low-level муть 2) выхлоп компилятора или тесты производительности могут отрезвить в некоторых случаях

Но там, где уместно, полиморфизм вполне себе решение. Случай с диалектами - зависит от применения.

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

я думаю, что он как бы думает, что

Не надо за меня додумывать.

Виртуальные вызовы дешёвые

Смотря с чем сравнивать. Но в целом - да.

Надежда на девиртуализацию

Оно и так нормально работает.

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

Пожалуйста, в тактах процессора на вызов в случае сработавших branch predictor & L1 cache и в случае cache miss & branch misprediction.

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

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

Как автор высокооптимизированного json парсера, не соглашусь. И, кроме косвенного вызова, как уже говорил, и процессор (в рантайме) и компилятор встретят много трудностей с оптимальным выполнением «двух инструкций».

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

В целом, чтобы оценить влияние таких вещей, нужно достаточно глубоко заняться низкоуровневой оптимизацией хотя бы нескольких независимых задач (= кусков кода / алгоритмов). И тогда книжные и кухонные мудрости типа «компилятор оптимизирует лучше человека», «вызов функции сейчас дёшев», «в 2XXX году заниматься байтодрочерством? Ты спятил?» и прочее становятся смешными (= имеющие ограниченное применение).

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

Как автор высокооптимизированного json парсера, не соглашусь.

Бенчи в студию.

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

Нужно делать. Теоретизирование тут малополезно, как и малополезен и пересказ размномастных околопрограммистских мифов.

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

это не мифы, а очевидные вещи. Чтобы не быть голословным, я обращусь к Technical report on C++ perfomance, ISO/IEC TR 18015:2006(E). Конкретно про разницу виртуального вызова и не виртуального на Page 26 of 202 и далее пару страниц.

Я понимаю, что старовато и девиртулиазация спасает сейчас чаще, но всё же оно медленнее.

Но к топику слабо относится сий спор, да.

zamazan4ik ★★ ()

Какие диалекты, что за срань? В конструктор запихни четыре параметра. И четыре поля добавь.

Производительность пострадает ровно на 0%, зато читаемость вырастет на процентов 200.

C++ головного мозга...

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

Объясняю, зачем это решение не очень:

Каким образом мне пользователю предоставить уже готовые парсеры для CSV, TSV, etc. форматов? А с помощью диалектов это делается легко - на этапе создания скармливаете DSVwriter нужный диалект и всё.

И полей там не 4 будет, а штук 6-7.

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

Ну ты накурился чтоли? Твои пользователи такие дебилы? Я не знаю, что им мешает конкретно инстанциировать твой DSV парсер с чем им нужно?

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

В C# BTW, лучшая библиотека для парсинга Csv именно что делает, так именно что один класс наружу выставляет, у которого тупо вот в аргументах конструктора чары-делимитеры. Как-то проблем ни у кого нет.

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

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

Вот это плюсану.

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

Как-то проблем ни у кого нет.

Пока все диалекты совпадают с точностью до разницы в синтаксических единицах. А если где-то другой способ экранирования строк, то работать не будет.

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

Разбежку в количестве тактов процессора на «две инструкции» в оптимистичном и пессимистичном случае так и не оценили. А потом уже можно смотреть какую пользу может дать встраивание невиртуальной функции, по сравнению с косвенным вызовом по адресу виртуальной.

Темы для затравки

1) определение компилятором отсутствия сайд-эффектов у встраиваемой функции

2) величина контекста оптимизации

3) стоимость вызова функции

4) инвалидация регистров и содержимого по указателям после вызова (виртуальной) функции

5) планирование выполнения и использования регистров (OoO)

6) десятки их.

Если темы кажутся абстрактными, то достаточно всё это перевести в наносекунды времени выполнения или увидеть лишнее жонглирование регистрами и ненужные store/load в случае невстраивания функции в каждом конкретном случае.

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

Преждевременная оптимизация - корень всех зол.

Если у вас CSV будет парситься не 1мс, а 0.8мс, но при этом будет содержать в 5 раз больше кода - это успех?

Считать инструкции имеет смысл в криптографии. Даже декодеры изображений пишут без супер оптимизаций (к примеру эталонный openjpeg vs jpeg-turbo).

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

Очередное мифотворчество. Определять когда и какая оптимизация преждевременна (неуместна) - нужно иметь вкус и опыт.

Если CSV будет парситься не 1мс, а 0.8мс - то это успех, если это библиотечный код с нормальным API.

А количество исходного кода вряд ли будет больше. Разве что если раскручивать циклы и заниматься встраиванием самостоятельно.

В общем, виртуальные вызовы - это совершенно не «две инструкции вместо одной». Это всё, что я хотел сказать.

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

Определять когда и какая оптимизация преждевременна (неуместна) - нужно иметь вкус и опыт.

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

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

в libc нужно считать инструкции

По glibc не скажешь.

Исходники quake посмотрите тоже.

Как там в 90-х? Сейчас игры уже на JS пишут.

В общем, виртуальные вызовы - это совершенно не «две инструкции вместо одной». Это всё, что я хотел сказать.

Я говорил лишь о преждевременной оптимизации. Которая плодится любителями «вкуса и опыта».

Вашу либу на гитхабе я видел. Мне этого достаточно.

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

Ну не надо гнать. glibc серьезно оптимизировали, 3D-играми тоже занимаются (хоть и не на уровне quake, но и не всё на js).

А так да, говорить о тактах и наносекундах в этой задаче - это, мягко говоря, профдеформация.

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.