Комментарии 82
Но что, если в дальнейшем нам потребуется распарсить не только json, но и xml? Как решить данную задачу? Передавать дополнительный булев параметр isJson?
class A {
public:
A(const string& jsonOrXml, bool isJson);
};
А вариант использовать другой класс не рассматривается? Либо делать два разных метода в классе?
class A {
public:
A();
void ImportJson(const string& json);
void ImportXML(const string& xml);
};
А вариант использовать другой класс не рассматривается?
Но результатом должна быть одна структура/класс. То есть мы должны получить из разных форматов одну структуру
Либо делать два разных метода в классе?
По сути я это и предлагаю. Вместо конструкторов делать функции/статические методы
Если же инициализация объекта xml/json-данными будет отдельной ф-цией, эта ф-ция может и статус возвращать, и принимать дополнительные параметры.
Можно передавать не строку а контейнер.
Можно все разнести по разным классам с общей базой.
И еще пачка архитектурных решений.
А вот статические методы — зло.
using Json = string
using Xml = string
это все один и тот же string
Можно передавать не строку а контейнер.
Можно, но зачастую слишком муторно пилить по контейнеру на каждый чих, чтобы воспользоваться ими всего в одном месте
Можно все разнести по разным классам с общей базой.
По мне довольно плохое решение
А вот статические методы — зло.
А почему они зло?
Статические методы — зло — потому что вносят хаос и беспорядок. А в вашем случае вообще не решают проблемы.
Есть strong typedef.
А почему они зло?В общем случае, статические методы не зло. Но в данном случае они создают на ровном месте ненужное копирование (пусть даже перемещение, всё равно не бесплатное) только что сконструированного объекта. Надо следить, чтобы не накосячить с этим. Часто видел, что RAII-классы специально запрещают своё копирование/перемещение.
В любом случае, заботиться о скорости работы именно на этом этапе разработки программы бесмыссленно. В том смысле, что лучше убирать «бутылочные горлышки» точечно, а не жертвуя выразительностью ради производительности
Но результатом должна быть одна структура/класс. То есть мы должны получить из разных форматов одну структуруДля этого и существуют паттерны проектирования. Например — фабрика.
Если у нас вдруг добавится парсинг YAML? Тоже добавлять метод в класс? В идеале, конечно, иметь какую нибудь рефлексию и стандартные (де)сериализаторы в разные форматы. Но загрязнять класс парсингом из произвольных форматов, имхо, так себе вариант.
Для этого процесс сериализации отделяют от самого сериализуемого объекта. Один из примеров — библиотека Cereal.
А вариант использования Command отбрасывается?
A(const std::string& s, AbstractParser&), к примеру?
А если использовать специфики С++ с так Вами ругаемыми шаблонами, то можно получить ещё более выразительный код, который, к тому же, будет эффективнее.
template<typename Parser> A(const std::string& s);
Плюс, как я помню этот подход, там все равно используется фабричный метод, а не конструктор
В общем случае идея одна: грамотное проектирование — решение всх проблем, поэтому статья мне кажется неуместной и откровенно глупой, Вы уж простите. Все проблемы, которые вы перечислили не проблемы вовсе, если заранее думать как писать и использовать инструменты там, где их нужно использовать.
Проблемы с конструктором
В смысле деструктор не вызовется? Как это? https://isocpp.org/wiki/faq/exceptions#ctor-exceptions, да и вообще, Вам, наверное, стоит почитать про RAII.
Шаблоны.
Ну тут я вообще не понял, в чем проблема-то? Не используйте их, если в данном месте они не нужны. С таким же успехом можно нагнать на что угодно, вот смотрите: "макросы нечитаемый отстой", попробуйте доказать, что это не так. Да и вообще, вы знаете инструмент удобнее, чем шаблоны для генерации кода? Я — нет. Потому что других механизмов нет, и это, отнюдь, не боль для программиста, если программист знат где их использовать, а главное умеет это делать.
Соглашусь с нечитаемостью, да, синтаксис морально устаревает и с развитием языка становится всё более громоздким, но это всё не бОльшая претензия, чем говорить, что в Java очень длинные имена классов.
Кроме того, есть такая замечательная штука как концепты в С++20, если вас не устраивают шаблоны, посмотрите на них.
Виртуальные функции
Вот этот отрывок информации меня просто сразил. То есть Вы говорите, что вот, у нас в C++ должны быть только интерфейсы, что мы против полиморфного поведения, мы отрицаем добрую часть работы "Банды четырех?", все механизмы vtable — отстой, а stl с их basic_classname неправы и добавляют боли программистам (сюда же и поголовные шаблоны в stl)? Какая глупость.
Резюмирую: мне данная статья кажется некомпетентной. Всё Вами перечисленное — проблемы проектирования и неумения применять инструменты языка.
Вообще, мне кажется, что если есть какая-то вероятность десериализовать из нескольких разных форматах, то хорошо использовать какое-то промежуточное представление дерева, в который парсить и из которого разбирать.
Так получается, что есть у нас десятка три класса, умеющих разбираться из json. Светлым умам пришла идея читать из XML. В три десятка классов добавляем чтение? А потом плюс класс, добавляем в него две десериализации. Звучит грустно, если только вы не на почасовой оплате.
Но если очень надо всё делать в классе, то можно, наверное, так:
class Foo
{
public:
explicit Foo(JsonParser const&);
explicit Foo(XmlParser const&);
};
Зачем обязательно передавать строку?
void foo() {
auto obj = std::make_unique< object >();
obj->foo();
}
где тут вызов деструктора IDE найти сможет?из деструктора нельзя вернуть результат выполнения никаким образом, даже через исключение.Думается, или понимание деструктора не корректно или архитектура.
Это удаление объекта, ни чего более. Нельзя удалить обхект на половину и сказать «я дальше я не шмогла».
Собственно следующее, как по мне, говорит именно об архитектурных проблемах:
также часто бывает, когда объект «застревает» где-нибудь в кэше, из-за чего вызова деструктора можно не дождаться вовсеДеструктор не может быть не вызван. Объект или удаляется или нет, С++ не имеет сборщика мусора.
Я понимаю, что «если нельзя, но очень хочется..» — но здесь нужно принимать все последствия подобных «лайфхаков» на свой страх и риск.
И, увы, файл можно закрыть наполовину.
А зачем знать все места, где вызовется деструктор? Мы знаем правило, что когда закончится лайфтайм. Если где-то объект зависает, то это ж не проблема деструктор а.
Как сказать.. С++ сложный. И гибкий. Это его достоинства или недостатки? ))
Причем у программиста должна быть возможность легко избавиться от шаблонов как в одном определенном месте, так и во всем проекте целиком.
В смысле? Подменить std::vector на свою реализацию? В чем вообще проблема шаблонов? Кроме того что их можно криво написать т.к. это проблема любого языка а не шаблонов в частности.
Подменить std::vector на свою реализацию?
Тут довольно тонкая грань, где от шаблонов избавляться стоит, а где не стоит. Конечно, от vector избавляться не нужно, так как он несет в себе очень много преимуществ. Но заметьте, вся стандартная библиотека построена так, что вы можете подсунуть туда любую структуру данных, даже самописную и без шаблонов. Вот это — правильная архитектура. А представьте, если бы везде в библиотеке требовался бы именно vector
Кроме того что их можно криво написать
Много чего помимо этого. Замедление скорости компиляции, очень приятные ошибки на 10 экранов, невозможность отдебажить ошибку в отличие от обычного кода, высокий порог входа…
std::map (и не только он) к примеру использует std::pair в итераторе, заменить его не возможно, хотя очень хочется
99% шаблонов что я видел не были template< template< > > так что я утверждаю что большинство шаблонов принимают обычные типы.
Взять тот же boost::fusion — им можно пользоваться не написав ни одного шаблона. Это будет куча повторяющегося кода но всё таки оно заведётся, вопрос в том будет ли от этого код лучше.
Высокий порог входа — согласен, как мне кажется он выше чем мог бы быть именно из за заявлений "шаблоны не нужны".
Замедление скорости компиляции — покажите мне вариант без шаблонов выполняющий те же самые операции и при этом компилирующийся быстрее.
Ошибки на 10 экранов — соглашусь частично, С++ так уж устроен что даже когда ты случайно вместо double втыкаешь int ты можешь получить километр ошибок. Шаблоны обычно пугают этим ещё больше т.к. их вывод обычно длиннее. Работать с ошибками в шаблонах надо так же как с и ошибками в коде без шаблонов — смотрим на первую, исправляем, остальное игнорируем. В большинстве случаев проблема шаблона видна в первых 8 строчках даже если вы любите шаблоны с кучей аргументов. Согласен, есть случаи когда разваливается SFINAE и в предельном случае приходится читать тонны вывода чтобы понять что не так, опять таки — грамотное проектирование шаблонов может сократить такое количество случаев, например стратегически расставленными static_assert.
Невозможность отдебажить шаблон спорная — с одной стороны стандартные компиляторы такой фичи не имеют, с другой стороны вроде как в студии уже что-то для этого появилось, и отдельные утилиты для этого имеются. Ну и в принципе шаблоны очень легко покрываются тестами (которые даже запускать не надо).
Кодогенерация в С++ осуществляется внешними инструментами и ни как не относится ни к компиляции, ни к шаблонам. Я могу привести примеры не удачного SFINAE когда различные шаблоны действительно дают разное время компиляции, проблема в том, что за исключением тривиальных случаев SFINAE вырождать в руками написанные типы мягко говоря не практично.
Я согласен что можно наговнокодить на шаблонах, так же как на функциях, замыканиях, классах, goto, циклах и даже структурах. Вопрос был почему вдруг шаблоны сами по себе стали плохой штукой.
Я видел как код вообще без шаблонов выродился в плохо поддерживаемого монстра из за бездумного пихания ООП везде где не надо.
Например, функции f2 могут быть нужны элементы A1 и A3, и не нужен элемент A2. Зачем же тогда его указывать в заголовке функции?
или просто их не использовать? Кстати, structured bindings вам в помощь
Но при этом, выбросив из конструктора исключения, вы должны помнить, что деструктор данного элемента вызван не будет.
вас не должно удивлять то, что не удаляется объект, который даже не был создан
Но что, если в дальнейшем нам потребуется распарсить не только json, но и xml? Как решить данную задачу? Передавать дополнительный булев параметр isJson?
сами проблему придумали, сами её и победили... То, что конструктор создает объект, не значит, что его нельзя создать, подготовить и вернуть из функции.
Во-первых, из деструктора нельзя вернуть результат выполнения никаким образом, даже через исключение.
во-первых, можно (noexcept(false) и кидаем), во-вторых, это запрещено по умолчанию и не приветствуется не просто так. Например потому, что в случае вызова деструктора в процессе раскрутки стека из-за исключения программа прервется (т.к. не может быть двух исключений одновременно). Это даже не говоря о том, что RAII спроектирован для того, чтобы вы не задумывались о ручном разрушении объектов, а в случае с кидающими деструкторами вам придется об этом думать.
Во-вторых, деструктор обладает такой неприятной особенностью, как невозможность отследить все места в коде, из которого он будет вызван
опять же, вы не должны хотеть пытаться отследить вызовы деструктора
Во-первых, следует разделять RAII и не-RAII деструкторы
прошу прощения, но это глупо, неправильно, и ересь. Деструктор нужен с одной целью - освободить захваченные ресурсы. Если вам нужно одновременно А. навешивать на деструктор логику с не гарантированным исполнением и Б. отслеживать все места в коде, где вызывается деструктор, то возможно, вы на самом деле хотите сделать отдельный метод и вызывать его там, где пожелаете?
Что же делать с деструкторами, которые не являются RAII? Здесь можно обратиться к языку rust и увидеть, что в нем нету требования на обязательный вызов деструктора. Деструктор может как вызываться при выходе из скоупа, так и не вызваться, если мы его вызов где-то раньше отменили. Я предлагаю действовать похожим образом.
это называется "утечка" и в общем случае является ошибкой. И это абсолютно точно является ошибкой если вы хотите отменять деструктор в случае ошибки.
вы очень подробно написали процесс борьбы с ветряными мельницами, вот только на вопрос "а зачем это нужно?" не ответили.
Для того, чтобы писать более простой и понятный коду вас проблемы надуманные, а методы решения по большей части неортодоксальные. Я, пытаясь оценить ваш пост с точки зрения плюсовиков разного уровня, понимаю, что те, кто теоретически мог бы допускать озвученные ошибки, просто не поймут половину советов, а вторую половину лучше бы и не понимали.
Зачем это сделано в rust например? Там многие идеи выглядят похожим образомкакие идеи то? Создание объекта через конструктор и специальным методом не шибко принципиально отличается (ну, кроме инициализации базовых классов, но в расте такого концептуально нет). Это не делает конструкторы какими-то плохими. Вот возьмем ваш же пример:
class A {
public:
A(const string& jsonOrXml, bool isJson);
};
class A {
A(const string& jsonOrXml, bool isJson);
public:
static A from(const string& jsonOrXml, bool isJson) {
return {jsonOrXml, isJson};
};
};
Или ваш пример с отменяемыми деструкторами… Вот например я могу написать такую вот ахинею:
void forget(std::move_constructible auto& v) {
using T = std::remove_cvref_t<decltype(v)>;
typename std::aligned_storage<sizeof(T), alignof(T)>::type s;
new (&s) T(std::move(v));
}
Я чё-т не уверен, что у вас там не UBконечно же UB, но мы и добиться хотим херни. Например мы можем добиться проезда если там была циклическая ссылка. Ну можно в куче выделять конечно...
Сторедж для объекта сдыхает раньше, чем его лайфтайм заканчивается, разве нет?идея и была в том, чтобы «выключить» деструктор, то есть лайфтайм никогда не заканчивался, а сторедж всё равно освободить надо.
Однако такой функции не просто так нет — просто «забыть» про ресурс это всегда ошибка.
Простите, а как вы предлагаете реализовать вот такой метод без использования forget
/ManuallyDrop
?
Однако rust-версия вызывает у меня полное недоумение. Во-первых, зачем вообще нужен Box<[T]> и зачем в него конвертировать? Почему нельзя было реализовать метод так же, как его сделали бы в плюсах? Почему нельзя было просто достать Box<[T]> из RawVec внутри Vec? Почему там вызывается shrink_to_fit, который еще и реализован не через realloc? Половина приседаний кажется попросту лишней…
Если его достать то он будет связан владением с вектором. Box<[T]> имеет фиксированный размер, поэтому если ему дать кусок памяти большего размера то память немножко утечет.
Идея в том что Vec динамический, так что можно удобно собрать вектор а затем превратить его в бокс массива
Если его достать то он будет связан владением с вектором.ну по сути это чисто семантическое решение чисто семантической проблемы. Причем довольно-таки костыльное, как по мне. Могли бы ввести возможность помечать методы в качестве разрушающих, и осталось бы только переопределить лайфтаймы объектов (это раст вроде умеет?)
Box<[T]> имеет фиксированный размер, поэтому если ему дать кусок памяти большего размера то память немножко утечет.аллокаторы немножко не так работают — освобождается весь буфер независимо от того, каким вы считаете его размер.
Идея в том что Vec динамический, так что можно удобно собрать вектор а затем превратить его в бокс массиваНо зачем? Сэкономить 8 байт на стеке? Ценой копирования вектора?
Могли бы ввести возможность помечать методы в качестве разрушающих
Мне кажется подобное поведение в принципе в расте сделать можно, но в конкретном случае из за динамической природы vec оно практически бесполезно — практически всегда размер вектора не совпадает с размером его буфера.
Как работает аллокатор в расте я признаюсь не в курсе, однако если я обнаружу что конструкция "массив из 4х int" занимает 20kb я сочту это ошибкой. То же относится к вопросу с resize — я не знаю поддерживает ли растовский аллокатор resize в принципе, и какие минусы при downsize, они могут быть.
Но зачем? Сэкономить 8 байт на стеке? Ценой копирования вектора?
Предположим у вас идёт накопление результатов, уже при сотне чисел в векторе вы можете как серьёзно сэкономить на ресайзах, так и неплохо сэкономить память конвертировав его в структуру минимального необходимого размера после того как сбор данных закончен.
Ну и по определению инварианта [T] он занимает памяти сколько минимально нужно, а не сколько попало, так что разрушать инвариант потому что можно в одном случае немного быстрее его получить смысла нет.
Мне кажется подобное поведение в принципе в расте сделать можно, но в конкретном случае из за динамической природы vec оно практически бесполезно — практически всегда размер вектора не совпадает с размером его буфера.std::Vec всё еще умеет делать shrink_to_fit(). Экономия получается ровно в размер одного указателя, т.е. 8 байт на большинстве систем. Это сравнительно мало, когда речь идет о ручке, держащей в куче M байт с погрешностью > 8.
То же относится к вопросу с resize — я не знаю поддерживает ли растовский аллокатор resize в принципе, и какие минусы при downsize, они могут быть.ну вот я и не могу понять почему в rust'овом Vec не используется realloc. В с++ проблема в нетривиальных типах — не зная заранее сработает realloc in-place или нет, написать корректное расширение/сжатие вектора через realloc попросту не получится. А проверять заранее будет сравнимо по стоимости с самой аллокацией, плюс придется реализовывать, ломать бинарную совместимость… Но в rust в Vec в принципе можно запихать только объекты тривиально муваемых типов, поэтому расширение/сжатие через realloc всегда должно быть корректным, независимо от того, сработало оно in-place или нет. Ну а бинарная совместимость при статической сборке мало кого волнует.
Так он и делает shrink_to_fit
let mut vec = Vec::with_capacity(100000);
vec.extend([1, 2, 3].iter().cloned());
assert_eq!(vec.len(), 3);
let slice = vec.into_boxed_slice();
assert_eq!(slice.len(), 3);
кажется вопрос что он должен делать shrink_to_fit закрыт.
Почему shrink_to_fit копирует а не делает realloc — полез я проверять это утверждение, и нашёл что он делает shrink https://doc.rust-lang.org/beta/src/alloc/raw_vec.rs.html#463, а как именно shrink выполняется — уже на совести выбранного аллокатора
В своей статье я дал ссылку на другую статью
habr.com/ru/post/460831
где обсуждаются теже самые проблемы с конструторами и приводятся теже самые выводы. Почему там проблемы не надуманные, а здесь уже надуманные?
Хотя в той статье я также вижу ваш комментарий, который если я правильно его интерпретировал, критикует данный подход
> В итоге оказывается, что побеждать надо было не конструктор, а плохую сигнатуру.
В случае с функцией/статическим методом этой функции можно дать любое релевантное имя. Как дать любое имя конструктору?
> который по большому счету будет делать плюс минус то же самое что и rust'овский mem::forget
Цель не в том, чтобы повторить mem::forget. Цель в том, чтобы не считать деструктор ВСЕГДА выполняющимся до конца.
В своей статье я дал ссылку на другую статьюЭто такое признание в плагиате?
habr.com/ru/post/460831
где обсуждаются теже самые проблемы с конструторами и приводятся теже самые выводы.
Почему там проблемы не надуманные, а здесь уже надуманные?не помню чтобы я говорил что по всем пунктам согласен с автором той статьи… Нет, согласен конечно с некоторыми аргументами, например про незавершенное состояние объекта внутри конструктора и про вызовы виртуальных методов из него. Но у вас такого нет. Однако у вас есть пункт про то, что вернуть ошибку из конструктора можно лишь исключением. Нюанс в том, что автор той статьи это проблемой не считает,
Это часто используется в качестве аргумента в пользу того, что использовать C++ без исключений сложно, и что использование конструкторов вынуждает также использовать исключения. Однако, я не думаю, что этот аргумент корректен: фабричные методы решают обе эти проблемы, потому что они могут иметь произвольные имена и возвращать произвольные типы
И уже помимо этого, сразу следом идут откровенно нелогичные выпады, например «о при этом, выбросив из конструктора исключения, вы должны помнить, что деструктор данного элемента вызван не будет.» — почему вы вообще решили что деструктор не созданного объекта должен вызываться?
В случае с функцией/статическим методом этой функции можно дать любое релевантное имя. Как дать любое имя конструктору?
constexpr struct FromXmlTag {} from_xml;
constexpr struct FromJsonTag {} from_json;
class A {
public:
A(FromXmlTag, std::string& s);
A(FromJsonTag, std::string& s);
};
// usage:
A x(from_json, text);
A y(from_xml, text);
Цель не в том, чтобы повторить mem::forget. Цель в том, чтобы не считать деструктор ВСЕГДА выполняющимся до конца.Во-первых, зачем такое может понадобиться? Во-вторых, если вам так нравится раст, покажите мне в сигнатуре трейта drop (что является максимально близким эквивалентом плюсовому деструктору) способ вернуть ошибку.
Извините, что я не знаю как вам объяснить необходимость свойства непрерывности у деструктора. Проблема в том, что я считаю это логически очевидным. Возможно, вам просто не стоит пытаться запихивать в деструкторы прерываемые операции?
В результате этого постоянно возникают ситуации, когда деструктор вызывается не из того места в коде, где предполагалось, и даже не обязательно из предполагаемого потока, также часто бывает, когда объект "застревает" где-нибудь в кэше,
Это, простите, сюр какой-то. Если писать на языке, который не знаешь, то объекты не только в кэше, но ещё и в сети застревать начнут. А потом их ещё и из регистров придётся выковыривать.
Скажите пожалуйста, вашу статью следует воспринимать всерьез? Выглядит она как своеобразная сатира. Вот и вынужден задать столь тупой вопрос, поскольку не могу понять, как же к ней относиться.
Я знаю, что вы любите шаблоны, но у меня другой подход к вопросу
С кортежами вы, конечно, перемудрили.
1) Кортежи, идентифицирующие поля по типам, не взаимозаменяемы с кортежами, идентифицирующими поля по индексам.
Что вы хотели сказать, что "если требования изменятся… чуть подправив код".
Не чуть. Вам логику программы придётся переделать. Это как множества заменить на векторы, или наоборот.
2) Сам код класса Tuple — оверхед на оверхеде. Цену кортежа функций себе представляете?
А ведь, судя по описанию, всего-то и хотелось — сделать неявное отображение произвольных кортежей друг на друга (на кортеж с более узким набором типов, естественно).
3) Сделать диагностику ошибок компиляции более вменяемой можно с помощью static_assert'ов. Да, это отдельный труд. И, фактически, это входит в интерфейс библиотеке — не API, а метаинтерфейс, для компилятора и программиста.
Точно так же, как диагностика ошибок времени исполнения может варьироваться от UB (в доках было сказано "не делайте так", вы сделали, так вам и надо!) до логгирования, продуманных систем исключений / кодов возврата и точек отладки.
Вы жалуетесь на то, что вам было трудно проделать эту работу как автору библиотеки, а потом было больно, как клиенту библиотеки? Или что?
Худшие места в C++ для написания кода