Комментарии 404
Логика у человека простая: языки, на первый взгляд, довольно близкие и почти обратно-совместимые, базовый синтаксис одинаков, про ООП кандидат что-то слышал, и значит, основная база уже есть и он сможет легко освоить C++ за 21 день в процессе работы, поэтому можно наплести про "с C++ тоже работал", начать писать на "Си с классами" и все получится.
Мне кажется, что такая логика может встречаться только у совсем уж новичков и я сомневаюсь, что хоть кто-то из более-менее опытных программистов на C может так думать или не знать про кардинальные различия между данными языками. Из перечисленного вами списка, многое может не использоваться намеренно, а не потому, что об этом не знают.
В общем очень спорные утверждения.
Как "программист на С" (в том числе) я подтверждаю. Для меня "современный С++" - тьма непроглядная, в которую мне вникать и вникать. Rust, кстати, в этом смысле проще.
Приходилось "поддерживать код" на новых С++ - писал именно как указанно в статье (ну кроме new/malloc - но только и-за того что неправильно смешивать парадигмы управления памятью в одной кодовой базе)
100% согласен. Поэтому для меня, как для Си-шника, Java, как ООП-продолжение, намного приятней, чем современный C++.
Подтверждаю. Я даже как начинающая студентка после относительного освоения C, плюсы были сложнее в освоении, чем понять концепции Rust и начать писать на нем. Хотя и понимаю, что работу найти пока будет сложнее.
На месте директора я бы сразу уволил такого менеджера в 24 часа, с пометкой "Больше его к техническим специалистам не допускать". И мне все равно, что всего за один дурацкий заголовок.
Я скорее энтузиаст, и предпочитаю использовать максимум API нежели готовые framework-и. Да, я художник, а не биоробот и с такими работаю максимум подрядом. Потому что не работаю с помешанными на перфекционизме диктаторами. Как я уже сказал, без зазрения совести такого менеджера нельзя допускать к разработке, даже если он карьерист и уже Team Lead. Либо ваш диструктивный подход скорее вредит разработчикам, чем помогает. И о вас уже есть целая статья.
Вот когда станете в ходе работы над собой более конструктивным, научитесь созидать, а не разрушать, то тогда можете написать, предложите интересный сложный проект, может и поработаю с вами. Я уже к тому времени проект выложу, над которым работаю сейчас.
>.... когда станете в ходе работы над собой более конструктивным
imho, все просто, это статья написана не для вас (и не для меня), честно говоря не совсем понимаю для кого она, но это не повод для волнений, опыт всегда прав, но у разных людей в разное время :)
Действительно, полыхает, но от того, что половина списка — карго-культ:
Используете простые указатели на функции вместо std::function;
Это очень разные инструменты для очень разных случаев. Советовать слепо применять аллоцирующий полиморфный враппер независимо от того, нужен ли он — это шикарно само по себе, даже если забыть о том, что половина случаев с указателями на функции — это интероп с C и std::function туда не пролезет при всем желании.
Используете простые enum вместо enum class;
Если у вас enum'ами задаются конкретные константы, которые часто надо куда-то передавать в числовом виде (тот же интероп), то от enum class больше попаболи, чем пользы.
Для функций, не кидающих исключения, не используете noexcept.
А вы можете доказать, что не кидающих? И что все, используемое в этих функциях, тоже некидающее? И используемое используемым all the way down? noexcept уместен в специальных функциях и критических местах, в остальных — опять же, больше попаболи, чем пользы.
Для конструкторов забываете explicit.
Для всех подряд?
Для деструкторов забываете virtual
Для всех подряд?
Если вам нужно вернуть из функции несколько разных значений (например, результат работы и/или код ошибки)
+ 21. Если вы используете коды ошибок вместо исключений.
Статья есть лишь краткое перечисление пунктов, а не огромный талмуд описывающий подробно все возможные случаи и границы применения.
Вы пишете "Как различить C и C++-разработчиков" и приводите безапелляционный список "делай так, а не так". А потом дети и сишники начитаются и начнут лепить explicit и virtual куда ни попадя, потому что "это С++, так в интернете написано".
Если Вы приходите в проект, то необходимо применять приемы, уже принятые ранее в проекте. Иначе Ваш код будет выглядеть кривой заплаткой на корпусе корабля.
Я хотел показать, что поиск кристально-чистого С++ - cника сродни поиска сферического коня. Многие коллеги в своем роде "мутанты" между этими двумя языками. И мне кажется намного важнее в случае рассмотрения кандидатур знание/умение применить перечисленные приемы, чем слепое безапелляционное следование им "я_типа_тру_С++". То есть важно смотреть не то, что по умолчанию применил человек при решении теста, а его способность решить поставленную задачу "с применением таких-то вещей".
Советовать слепо применять аллоцирующий полиморфный враппер независимо от того, нужен ли он — это шикарно само по себе
std::function
аллоцирует только при наличии контекста (например, захвата из лямбд). Полным аналогом void set_callback(std::function<int(int)>&& cb)
в чистом си будет void set_callback(int(cb*)(int), void* ctx, void(*ctx_deleter)(void*));
где ctx в общем случае тоже надо аллоцировать, ну и не забыть удалить. Так что совет "используйте std::function вместо коллбеков когда можете" более чем валидный.
Если он там есть — естественно, std::function проще, лучше и понятнее, чем любой ручной колхоз.
Однако, оверхед там есть и он далеко не нулевой, даже с оптимизациями. И noexcept, как автор рекомендует в 12 пункте, уже не прицепишь.
Поэтому все как всегда — используйте что угодно, если вы понимаете, как оно работает и можете себе это позволить.
Однако, оверхед там есть и он далеко не нулевой, даже с оптимизациями. И noexcept, как автор рекомендует в 12 пункте, уже не прицепишь.
Присмотритесь. В примере bar() отлично оптимизируется в noop, пара доп. инструкций в foo() и call std::__throw_bad_function_call() это проверка функции против null (то, чего сишная версия не делает, а если делать, то оптимизируется и там и там) а typeinfo сгенерился для всяких стектрейсов и убирается с --no-rtti. По сути весь оверхед сводится к заполнению std::function при инициализации, да и это важно только если ваш метод вызывает коллбек никуда его не сохраняя
Если против прыжка на NULL меня страхует MMU, то я не хочу тратить на проверку на NULL отдельные инструкции. Си позволяет их не тратить; а если хочется тратить, то позволяет и тратить.
если в конкретном случае вы замерили перф и оказалось что да, лишний if в std::function тормозит, то конечно же, делайте на указателях. Вот только под общий случай такой кейс вообще даже близко не подходит. Ну и от себя добавлю, что на практике я куда чаще сталкивался с недостаточно гибким интерфейсом (и костылями как его следствием), чем с тормозами в хорошо предиктящемся if
Вынужден вас расстроить: по крайней мере часть компиляторов вставляет эту проверку принудительно,.им им пофигу, что адрес 0x0 находится в секции с флагом x
clang.llvm.org/extra/clang-tidy/checks/bugprone-exception-escape.html ругнётся, если увидит явное кидание исключения (т.е. функции должны быть в header), но вызов `noexcept(false)` функции — не ошибка.
Казалось бы, проверить noexcept() всех вызываемых — легче, чем анализировать их код.
Если у вас enum'ами задаются конкретные константы, которые часто надо куда-то передавать в числовом виде (тот же интероп), то от enum class больше попаболи, чем пользы.
Можно ли этот тезис раскрыть более подробно?
Проблема простых enum'ов в отсутствии скопа и неявных преобразованиях, позволяющих смешивать разные enum'ы.
Комитет в своей бесконечной мудрости ловко решил обе проблемы одним выстрелом, добавив enum class.
Однако, это две сильно разные проблемы: отсутствие скопа — это (почти) всегда плохо, а вот неявное преобразование — плохо не всегда, однако его оторвали совсем и нельзя даже opt-in.
Традиционно enum используется для определения констант, как окультуренный аналог #define. Скоп здесь весьма уместен, дабы не пихать префиксы прямо в имена констант, и с легкостью внедряется банальным поиском и заменой. А вот с отстуствием неявного приведения типов все гораздо хуже: чтобы не писать static_cast на каждый чих, тип этого enum class надо протаскивать везде, где он используется, что в любом проекте сложнее helloworld порождает вопросы «а стоит ли оно того?». Проще обычный enum в struct или namespace положить.
Кроме того, enum используется еще и для определения флагов, а флаги не живут в ваккууме, их надо уметь комбинировать и разделять, поэтому надо не только протаскивать везде новый тип, но и перегружать операции над ним, что кажется легким только на первый взгляд.
Для понимания проблемы попробуйте переделать вот этот достаточно простой и идиоматичный код на enum class, желательно, не меняя слишком много (в реальном проекте миллион строк) и сохраняя идиоматичность (с кодом работает более одного человека):
enum flags
{
flag1 = 0x1,
flag2 = 0x2,
flag3 = 0x4,
};
int main()
{
auto state = 0;
if (some_condition)
state = get_some_external_state();
cout << "state is " << hex << state;
if (state & flags::flag1)
{
// something
}
if (state & flags::flag2)
{
// something else
}
set_some_external_state(state | flags::flag3);
}
а вот неявное преобразование — плохо не всегда
Это плохо всегда.
Традиционно enum используется для определения констант, как окультуренный аналог #define.
Традиционно где? В чистом Си?
Если мы говорим именно про C++, то в C++ использование унаследованных из Си enum-ов бесполезно чуть меньше, чем полностью (особенно после появления C++11 с enum class).
А дабы проблем, которую вы проиллюстрировали, не было, в C++ (еще со времен добавления в него namespaces) можно было делать так:
namespace flags {
typedef unsigned short type;
const type flag1 = 0x1;
const type flag2 = 0x2;
const type flag3 = 0x3;
}
Получаем и скоуп, и фиксированный тип, который скрывается за "перечислением". И имеем те самые неявные преобразования, которые выходцам из Си так нравятся.
Самый простой способ сделать нормальные флаги в с++ - std::bitset. Можно сделать тонкую обертку для enum class, переопределив operator[]. А все эти битовые трюки с флагами-степенями двоек были не от хорошей жизни придуманы.
Тем не менее, когда с таким приходится работать, то почему бы и нет. Тем более, что встречаются флаги, в которых должно быть выставлено сразу несколько битов, типа 0x82 или 0x71. Ну и до C++11 в заголовочных файлах константы через std::bitset было не задать, разве что вместо констант писать inline-функции, которые возвращали бы экземпляры std::bitset.
Тем более, что встречаются флаги, в которых должно быть выставлено сразу несколько битов, типа 0x82 или 0x71
так у bitset'а есть конструктор от числа, можно в обертку добавить конструктор от initializer_list<my_enum>, вариантов уйма
Боюсь, что все они будут гораздо объемнее, чем простое задание целочисленных констант. Ну и на компиляцию этого будет уходить гораздо больше времени.
Опять же, если речь заходит о старом коде, значительная часть которого написана во времена C++98, если не раньше, то там таких продвинутых фич не будет.
Тип то фиксированный, но общий и вполне интегральный.
Его наличие никак не помешает написать, например,
tCounter.store ( std::memory_order_release );
вместо
tCounter.store ( 0, std::memory_order_release );
Это бы простейший пример того, как в старых плюсах (старых -- это времен 1993-1994 годов, когда стандарта еще не было, а вот namespaces уже в компиляторах стали появляться) получить все тоже самое, что дает C-шный enum, но с парой дополнительных плюшек.
Чтобы обеспечить большую безопасность по типам в тех же старых плюсах (но уже когда там поддержка шаблонов появилась) можно было бы использовать шаблоны. Типа:
namespace type_safety {
template<class Scalar, class Tag>
class scalar_value {
...
public:
explicit scalar_value(Scalar v) ... {}
...
Scalar raw() const {...}
...
// Полный фарш включая перегрузку различных операторов.
};
}
namespace flags {
struct flags_tag {};
typedef type_safety::scalar_value<unsigned short, flags_tag> type;
...
}
Правда, у этого подхода были существенные недостатки.
Во-первых, все эти шаблоны негативно сказывались на скорости компиляции, а некоторые компиляторы при обилии шаблонов в коде могли тупо падать.
Во-вторых, нельзя было просто так в заголовочном файле размещать инициализированные константы для flags::type, т.к. можно было получить ошибку линковки.
Но там, где безопасность по типам была нужна, на такое вполне себе шли.
В старых плюсах у C-шного enum-а была большая область применения вот в таком контексте:
struct demo {
enum { value = 1 };
...
};
без необходимости бодаться со static-полями и их инициализацией.
Но в современном C++ для всего этого уже есть чисто C++ные решения. Так что какую пользу от С-шного enum-а можно получить, например, в C++17 -- это лично для меня большой вопрос.
Мне кажется, плюсовики - это такая секта.
Они готовы не только спросить о принципах ООП, но и уточнят в каком текстовом редакторе вы обычно работаете. Потому что не все из них "правильные".
Ну а навыком кодить на бумажке должен обладать каждый уважающий себя программист.
>12. Используете простые enum вместо enum class;
К сожалению, enum class - неполноценная замена enum: в том же QT enum используются для хранения флагов и констант из них/проверок на их наличие, и этого нельзя достичь с использованием enum class без повсеместного ручного приведения типов. Хуже всего, что нет нормального удобоваримого способа достать из него значение (без приведения типов или третьестопных малоизвестных шаблонов).
>15 Если вам нужно вернуть из функции несколько разных значений (например, результат работы и/или код ошибки), то одно из них вы возвращаете через return, а другое - по указателю или по неконстантной ссылки, вместо использования std::optional, std::pair/std::tuple (особенно хорошо в паре со structured binding) или просто возврата struct;
Есть ещё альтернативный вариант - сделать для этой функции класс, передавать параметры к ней как конструктор/использовать setter'ы, забирать результат через getter'ы. Очень удобно, когда хочется вернуть много отладочной информации и при этом не замусоривать синтаксис основного применения.
И это будет из пушки по воробьям. Синтаксический сахар должен уменьшать объем написанного кода, а не увеличивать его.
То есть, как у вас в конце статьи и написано: надо смотреть по конкретной ситуации.
Можно перегрузить
&, |, &&, ||, ^, &=, |=
у этого enum'а и работать с ними как раньше, или даже навернуть темплейт сверху для более универсального решения.
Можно, но зачем?
А я скажу, что после этого код превратится в легаси и в чем-то буду прав.
Если завтра к вам придет обычный разработчик, а не фанат плюсовой магии, как вы ему объясните, зачем, это было сделано?
Не, вы меня не поняли.
Обычный разработчик (не фанат плюсоизмов), на большинстве языков из топ10 не сможет или не будет это делать. Он просто напишет функцию is_read_only().
Как вы этому человеку объясните зачем вы это делаете:
Можно перегрузить
&, |, &&, ||, ^, &=, |=
у этого enum'а и работать с ними как раньше, или даже навернуть темплейт сверху для более универсального решения.
А код с бизнес-логикой писать хоть один из этих людей будет?
Один отстреливает себе ноги.
Другой расширяет язык, чтобы он в неочевидном месте неочевидным способом семантически был похож на другой язык
Третий разбирается в причинах трехэтажных ошибок шаблонов из-за того что он написал bit | flag для типа которому оператор | не перегружен его коллегой сверху
Последний спорит в комментах на хабре
Нет, нельзя, скрпы эмбеддеров это запрещают!
Шутка. Я бы конечно все просто писал на расте. Я пробовал и под МК без MMU писать и под арм64, все отлично, удобно и комфортно.
Просто есть отдельная часть людей, которые считают что под МК надо писать только на Си, а под армы с MMU - на С++. Причины и доводы, как обычно, те же самые, но вешать ярлыки не будем.
Просто почему-то этих людей совершенно не смущают известные и успешные проекты, которые были сделаны не на С/С++.
А как бы мы жили без Java Card - вообще не понятно.
Ещё бы был у это Раста синтаксис адекватный а не вырвиглазный.
В общем С++ лучше, к тому же с активным развитием современных стандартов вряд ли Раст особо сможет потеснить ++, т.к. и там приемлемо безопасно и адеквантый синтаксис схожий с Java и C# - а это большой плюс при выборе яп.
Например когда Джавистам или Шарпистам потребуется высокая производительность - вряд ли они выберут Rust, но выберут С++ т.к. им его будет значительно проще освоить.
у ржавого синтаксис по началу действительно странноватый. но это скорее из-за новых концепций, нежели синтаксических конструкций. аналогичные вещи на плюсах как правило выглядят более тяжеловесно. как яркий пример из относительно свежего оператор <=>
Ещё бы был у это Раста синтаксис адекватный а не вырвиглазный.
А что конкретно, по вашему, не так с синтаксисом Rust?
я написал своё мнение, т.к. лично для меня чем дальше язык по синтаксису от Си++ тем он уродливее (именно от ++, а не от родоначальника синтаксиса – Си). В т.ч. мне не нравятся и Питон например. Но нравятся Джава, Шарп и Делфи (хоть он и не из Си семейства, но за счет многословности – а многословность неплохо описывает сам язык без документации, читать Делфийский код можно без предварительного изучения я.п, что не скажешь о Расте или Питоне например).
А вот Си мне не нравится например, причем сильно. Потому что на больших проектах (например Gimp) или там OpenVPN хорошо видно что языку не хватает того что есть в ++, в следствии чего например усложненные интерфейсы по сравнению с ++ (по кол-ву входящих параметров), обилие похожих друг на друга интерфейсов, отличающихся префиком/постфиксом (т.к. нет шаблонов и перегрузок - то часто на каждый тип входящих параметров или возвращаемых значений делают отдельный интерфейс).
Можно перегрузить &, |, &&, ||, ^, &=, |=у этого enum'а и работать с ними как раньше
Можно, но «как раньше» не будет.
Если вы, например, перегружаете & для некоторого enum class, моделирующего битовые флаги, то из оператора вы, вероятно, тоже возвращаете этот же тип, а не какой-то голый int, иначе ради чего это все. Однако, enum class неявно не конвертится ни в bool, ни в int и добавить к нему это нельзя, поэтому вместо
if (Value & my_flags::foo)
придется городить
if ((Value & my_flags::foo) != my_flags::zero_flag)
или вообще
if (flags::is_set(Value, my_flags::foo))
что, конечно, реализуемо и даже безопаснее, чем раньше, но выглядит неидиоматично и набирать эти простыни никакой радости.
enum class — это «хотели как лучше, а получилось как всегда».
Вообще не особо понятно, зачем автор предлагает из монструозного и сложного С++ костылями в одном месте сделать простой и лаконичный С, если до этого целую статью объяснял какие сишники смешные, что не хотят менять свой работающий подход кодирования определенных вещей (совместимый по стилю и семантике с другими популярными языками) на очередной новый стиль С++420.
Можно перегрузить "!" ?
enum class E { Zero, A };
bool operator!(E e) {
return e!=E::Zero;
}
int main() {
E e = E::A;
if (!!e) {
return 1;
}
}
В Qt есть Q_FLAGS/Q_DECLARE_FLAGS
для генерации типобезопасной обертки над enum class описывающим флаги.
интересно, через сколько в комментарии к статье придут любители Rust?
Явная провокация. Ну вот к чему это было? Не зря в комментариях в основном критическое восприятие статьи.
Используете функции из <time.h> вместо std::chrono.
Сразу вспомнил вот эту картинку:
C: gettimeofday(), clock_gettime() . Уверен, что и другие способы найдутся.
Обе эти функции не из стандартной библиотеки C, на Windows, например, они не существуют.
Но по факту, time уже почти ни кто не использует. Кроме уж совсем простых случаев. Если есть возможность, используют clock_gettime, т.к. только он возвращает MONOTONIC. Если реализация POSIX уж слишком древняя, мирятся с gettimeofday и возможным дрифтом часов, но зато хоть миллисекунды получаю
На Windows наверняка тоже есть способы. Легкий Гуглинг показывает GetSystemTime, GetSystemTimeAsFiletime и GetSytemTimeAsPreciseFileTime. И для монотонного времени QueryPerformanceCounter и QueryPerformanceFrequency .
В общем, начинает казаться, что уж лучше пусть будет несколько функций в стандартной библиотеке, чем несколько в нестандартной.
Кстати, POSIX - это стандарт, или не стандарт?
Формально - стандарт. А по факту :-(
Ещё мне кажется, C++-ные пуристы со своими наездами не понимают, до какой степени их язык паразитирует на сишном фундаменте. Убери его — и язык из топа быстро скатится на дно рейтинга. Рядом есть другие языки и у них стандартные библиотеки не такие упоротые.
Я настолько еретик, что пишу в резюме C/C++/C#
Чернокнижник!
У меня есть пара проектов, где в C# unsafe ставится целиком на классы, причём почти на все. Вот там C/C++/C# вполне валидная категория )))
Используете C-style массивы вместо std::array;
А это как? Ведь передача `int a[]`, `int a[20]` и даже `int a[static 20]` это всего
лишь передача указателя, как можно передачу указателя перепутать с передачей самого массива?
Используете функции из stdio.h вместо ... потоков ввода-вывода
Так потоки ввода/вывода в C++ были всегда тормознее Cшных вариантов. А умение clang/gcc проверять строчку форматирования во время компиляции сводило преимущество в типизации к нулю. Что-то изменилось?
А это как? Ведь передачаint a[]
,int a[20]
и дажеint a[static 20]
это всего лишь передача указателя, как можно передачу указателя перепутать с передачей самого массива?
Не совсем. Можно делать так:
template <class T, size_t N>
void Test(const std::array<T, N>& arr);
template <class T, size_t N>
void Test2(const T (&arr)[N]);
Так потоки ввода/вывода в C++ были всегда тормознее Cшных вариантов.
Дело даже не в тормозах, а в такой неприятной штуке, как глобальное состояние, которое приводит к широкому спектру ошибок:
https://stackoverflow.com/questions/2273330/restore-the-state-of-stdcout-after-manipulating-it
Ну и достаточно уникальная реализация потоков в C++. Вызов printf
хорош своей универсальностью: во многих языках он работает одинаково. Альтернатива printf
— позиционные аргументы (типа print "i = {i}, j = {j}"
). А вот аналога C++ потоков нигде нет.
опытный плюсовик-затейник, который будет активно внедрять best practices
Переход на Java, Python или Javascript?
Ну если память не экономим и скорость не оптимизируем (или оптимизируем?), то можно хоть PHP, нет?
C# вот отлично работает в embedded. Пробовал на плате с 4ядерным ARM процессором и 2 Гб оперативы. Нужно было сделать MVP, который потом подрядчик не сильно улучшил переписав на C++. Работать стало конечно быстрее, но конечный потребитель разницы не заметил.
а когда у вас 500 мегагерц и 256 мегабайт, то шикануть уже не получиться
У меня когда-то был компьютер с amd k6-2/500 и 128 мегабайтами памяти. Так вот там был сайт на пхп с mysql, еще кучка сервисов, которые обслуживали около сотни человек.
Современный ПХП еще в несколько раз быстрее той пятой версии, что я тогда использовал.
Как-то вы совсем недооцениваете возможности железа.
2 ГИГА ОЗУ в Эмбеддед.
Чтобы я так жил.
Да вы там Ноду, прости господи, ДжиЭс можете использовать.
А у меня 32 КиБ ОЗУ и лапки.
Хм, в моём мире Embedded - это встраиваемые решения. Типа банкомата, бортового компьютера и прочее, на них может работать линукс. 32 кб ОЗУ - это микроконтроллер
Вот вы смеётесь, а существует и довольно популярен python для микроконтроллеров: MicroPython
>опытный плюсовик-затейник
как-то не пришлось таких встречать, приходилось видеть:
либо опытный плюсовик,
либо плюсовик-затейник,
либо опытный затейник,
но это конечно не значит, что такое животное не существует, просто пока не обнаружено :)
как обычно, imho
Возможно Вы имеете в виду gumbo?
https://www.neworleans.com/restaurants/traditional-new-orleans-foods/gumbo/
Скорей как начало анекдота. Возможно даже про поручика, да.
Понятный и надежный код, Embedded Linux, C++ - вы серьезно это все поместили в одно предложение? Особенно применительно к микроконтроллерам и штукам класса зарядка для электромобиля? Где гораздо уместнее смотрелось бы RTOS+MISRA C хотя бы...
Если в самой зарядке RTOS тогда вопросов нет, коммуникационный узел на Linux как раз понятно, что будет оптимальным. Я просто попробовал представить себе Linux, управляющий 30 кВт зарядкой, особенно когда что-то пошло не так.
Наконец из вашего текущего комментария стала понятна простая вещь. Вы пишете под Linux. В область Embedded разработки входит еще и огромный пласт разработчиков под мк и их вы не убедите использовать C++ в своих проектах, ибо под всеми контейнерами уже не поймешь сколько у тебя памяти и на что ушло. Напишите в шапке или вначале статьи что вы имеете ввиду только Linux и половина хейта к вам отвалится автоматом. Под камень с MMU всем очевидно что нужно пользоваться всеми прелестями готовых плюсовых контейнеров.
Я однажды заглядывал внутрь с++ фреймворка Mbed для STM32, когда оказалось, что изменение GPIO пина с Input на Output занимает на нем 600+ тактов... Всего-то несколько слоев классов, да.
Идиоматичные обёртки на C++ могут быть вполне себе zero-cost, на хабре есть несколько статей на тему, вот одна из них.
С другой стороны, (не знаю, как сейчас, но как минимум несколько лет назад) STMовский HAL был весьма тормозящей либой, даром что сишной и даже написанной производителем железок.
Вот это пояснение что, где и как используется очень не хватает в статье. Без этого пояснения статья выглядит как минимум тонким троллингом и слегка провокацией.
>Понятный и надежный код, Embedded Linux, C++ - вы серьезно это все поместили в одно предложение
встречается, но не часто, редко бывает оправдано, и дело не в количестве протоколов, которые к C++ прямого отношения как правило не имеют, а скорее в сложности взаимодействия большого количества процессов в реальном времени, типа система из сотни устройств координирует работу, приходилось участвовать, удовольствие ниже среднего,
как обычно imho
ps
>представить себе Linux, управляющий 30 кВт зарядкой, особенно когда что-то пошло не так.
как насчет spacex в случае когда что-то пошло не так?
Ещё и с аппаратным Watchdog'ом... ;))))))))))))))))))))))
Хм... Оказывается, я "матерый плюсовик". С большинством из пунктов я согласен, но с некоторыми конкретно хотел бы поспорить.
Реально я "олдскульный плюсовик". На "чистом С" почти не писал, только в студенчестве, в самом конце 80х. Основной опыт - середина 90х - конец 2009, подавляющее время работы писал на MSVC++, в то время с библиотекой MFC. Сейчас много нового, смотрю, но, изучать уже не буду... Вобще от программирования отошел...
Похоже комментаторы выше рассматривали 20 пунктов как совет к действию, это же просто шаблон для определения си с классами. Поэтому можно согласиться с каждым пунктом, но при этом и использовать исключения из каждого. Статья полезная, но уж слишком много провокаций.
В Quake3 от C++ только классы но хуже он не стал. Уже сто раз видел проЭкты с невероятными мета гуру C++ которые через пол-года убегают оставляя после себя «чукча писатель не читатель».
Мне кажется, Q3 это одновременно очень плохой и очень хороший пример.
Очень плохой он потому, что при мысли об исходниках Q3 на ум в первую очередь приходит Fast inverse square root — ведь именно там же он впервые широко засветился. Такие вещи Кармак и компания делали явно не от хорошей жизни — компьютеры в те времена были намного слабее нынешних. И, следовательно, даже тот копеечный оверхед, который даёт, например, стандартная библиотека, мог играть для них принципиальную роль. Даже сильно позже, намного ближе к нашим дням, я обращал внимание, что писатели движков косо поглядывают на нововведения именно из-за привычки считать такты. Что поделать — эти люди сами выбрали мученическую стезю )) Но для нас, для большинства, эта разница не должна играть большую роль, а значит оглядываться на игропром, тем более такой старый, не очень продуктивно. Жизнь заставит — и на ассемблере будешь программировать!
А хорошим его делает… скажем вежливо, напоминание, что ЯП, вообще-то — инструмент для создания практически полезных программ. Таких как Quake 3, да. Или ядро Линукса. Кто может — тот делает, а кто не может — учит нас «совершенному коду» и «модерновому сиплюсплюсу». Я помню, как в своё время прочитал широко известную в узких кругах книгу Джеффа Элджера. Такую, с чёрненькой обложкой. Вы должны помнить — умные указатели, гомоморфные иерархии, вот это всё. Конечно, ходил под впечатлением: какой вумный дядька! Такое тройное сальто с переворотом и поцелуем себя в задницу в верхней точке при прыжке через горящий обруч — это ж не каждый сделает! А сейчас я поискал 'jeff alger' и ничего толком не нашёл. Вики про него не знает. Что он вообще сделал-то? Книжку написал?
Но само по себе это ещё полбеды. Настоящая беда в том, что эти астронавты летают в настолько разреженных слоях атмосферы, что им оттуда нас, убогих, с нашими повседневными проблемами, очень плохо видно. Процитирую я основоположника, который поставил C++ на те рельсы, по которым он теперь и катится — Александра свет Александровича. Учёного, как рекомендует его Википедия.
Я уверен, что ООП методологически неверна. Она начинает с построения классов. Это как если бы математики начинали бы с аксиом. Но реально никто не начинает с аксиом, все начинают с доказательств. Только когда найден набор подходящих доказательств, лишь тогда на этой основе выводится аксиома. То есть в математике вы заканчиваете аксиомой. То же самое и с программированием: сначала вы должны начинать развивать алгоритмы, и только в конце этой работы приходите к тому, что вы в состоянии сформулировать четкие и непротиворечивые интерфейсы.
Вот так! Теперь вы знаете, почему горячо рекомендуемый в статье std::string ничего не умеет, а алгоритмы навешиваются на него как амбарный замок. Александра Александровича нимало не смущает тот факт, что типичный программист вообще редко пишет алгоритмы (в том смысле, который вкладывает он в это слово) — типичный программист ими в основном пользуется. И что старый, недобрый, пропахший нафталином CString действительно облегчал программисту жизнь при переходе от char*: он, во-первых, худо-бедно, реализовывал абстракцию «строка». Можно было не думать, Юникод там внутри или ещё что-то. (Да, макросы _UNICODE — это, наверно, не очень хорошо. Но это решение! А наличие ДВУХ базовых типов строк — это просто отказ от всяких попыток решить проблему). Во-вторых, он умел весь джентльменский набор по работе с текстом — поиски с обоих концов, замены, капитализации и прочее — прямо внутри класса. Может быть, кто-то думает, что мы таскаем классы просто по факту наличия? Нет. Класс должен приносить пользу. И, в-третьих, что немаловажно, создатели подумали о совместимости с Си. То есть, о том факте, что как на сиплюсплюсе не пиши, а рано или поздно придётся вызывать WinAPI (или API другой ОС). И заложили в него operator LPCTSTR(). Вызов `::CreateWindow(strClassName, strWindowName...` однозначно считывается как `:: СоздатьОкно(оконныйКласс, заголовокОкна...` Напротив, степановское детище заставляет писать так: `::CreateWindow(strClassName.c_str(), strWindowName.c_str()...`. Что в переводе на русский означает: `:: СоздатьОкно(контейнерСодержащийОконныйКласс.извлекаем_строку(), контейнерСодержащийЗаголовокОкна.извлекаем_строку()...`. Конечно, это сделано в интересах трудящихся. Нас же хлебом не корми, дай лишний метод вызвать руками, чтобы напомнить себе, что мы тут не в бирюльки играем, а пишем на взрослом языке. Народ и начал разбегаться, как только стало куда — Java, C#, you name it. Осталась кучка самых стойких, из которых половина просто привязана к сишным API, и воленс-неволенс вынуждена досмотреть этот цирк до конца — так и до тех доковырялись. Плохо, дескать, летаем. Низэнько.
Вы собираете свои emb проекты на ядре linux, написанного на С. Т.е. могу предположить, что как и в большинстве подобных проектов это уже 80% кода. Туда еще можно положить немаленький u-boot или его альтернативу. А потом заявляете, что мы вот, без обид образно говоря ), по CAN гоняем 10 кодограмм и десяток алгоритмов бизнес-логики крайне мемори и типо безопасно т.к. не контрибьютим ни строчки "warning: use of old-style". Так у вас де-факто 90% кода уже в опасности ). Вам надо срочно переписывать ядро! По крайней мере загрузчик уж точно можете написать, он же проще пареной репы.
Поэтому в проекте разрабатывают под Embedded Linux и в качестве основного языка используют C++
Есть Cortex M4. 144 мгц и 160 к ОЗУ на борту. Необходимо поддерживать GSM модуль, FM микросхему. С FM Постоянно мониторить звук с эфира на предмет выловить кодовую посылку DTMF. В это же время обслужить микросхему Ethernet. При необходимости обслужить флеш память. По двум, трем каналам АЦП нужно мониторить звук на предмет поступления кодовой посылки DTMF. По команде транслировать звук из выбранного канала на УНЧ, или на выбранный выходной канал. Звук для обеспечения приемлего качества должен быть хотябы 24к семплов в сек.. Также необходимо мониторить солнечную панель и заряд аккумуляторов. И все это работает в режиме реального времени 24 часа в сутки. То о каком Ebedded Linux Здесь может быть речь? Только RTOS. Динамическая память не получается, так как некогда собирать мусор в памяти.
Как различить C и C++-разработчиков по их коду? Элементарно. У одного код написан на С, у другого на С++. С уважением, Капитан Очевидность.
по сей день нередко можно встретить Си-программу которую удастся скомпилировать C++-компилятором вообще без проблем или с самыми минимальными модификациям
Тут вы можете сильно ошибаться. Что разрешено в С, часто включается в С++ по умолчанию в msvc или через gnu-extensions, но валидным С++ кодом не является, отсюда получается то, что в Сях ок, в плюсах - UB. Оно скомпилируется да, но работоспособность ничем не гарантирована.
Ну впрочем я пишу в основном что-то алгоритмическое — из файла данные взяли, посчитали, запихнули обратно в файл
Забавное описание. Я вот тем же самым занимаюсь, разве что в «боевой» конфигурации обычно чтение идёт потоком с камер, и результаты передаются какой-нибудь управляющей системе по какому-нибудь специальному интерфейсу. Но этот ввод-вывод совсем незначительную часть кода занимает.
Могу ответить вам на этот вопрос. Необходимость плюсов по сравнению с pure С начинаешь понимать когда проект становиться больше определенного размера, когда его нужно поддерживать и развивать.
Я преписывал код С на плюсы несколько раз именно из за того что при малейшей модификации вылазило куча проблем. Классический пример это освобождение памяти, когда на плюсах, при условии выполнения нескольких правил, можно вообще не задумываться об освобождении памяти при выходе из блока. А вот в С надо обязательно отследить все динамические переменные и их освободить. А когда внутри кода несколько ветвлений то освобождение надо делать в нескольких местах либо усложнять код чтобы все сводилось в одну точку.
либо усложнять код чтобы все сводилось в одну точку.
А goto
в нужную точку в хвосте функции для освобождения не помогает, как в ядре Linux часто делают? Или ресурсы часто не так линейно вкладываются?
* даже в том же Rust я обнаруживаю, что очень часто fn (Rust-аналог сырого указателя на функцию) предпочтительнее Fn (Rust-аналог std::function), и я говорю не об интеропе.
Вы что-то путаете, причём капитально. fn
— это всегда функциональный указатель и всегда размером с usize
. А вот Fn{, Mut, Once}
— это трейты, и их использование совершенно не обязательно подразумевает аллокацию памяти. Вдобавок, каждая конкретная функция в Rust имеет свой собственный тип нулевого размера, и использование обобщённых типов с Fn*
-ограничениями позволяет сохранить нулевой размер в памяти, в отличие от функциональных указателей:
struct FnWrapper<F: Fn()>(F);
fn f() {}
fn main() {
use std::mem::size_of_val;
assert_eq!(size_of_val(&FnWrapper(f)), 0);
assert_ne!(size_of_val(&FnWrapper(f as fn())), 0);
}
Зачем такой спор в комментариях? Элементарно - пишите на C - вы молодец, оставьте его себе и пользуйтесь на здоровье.
Пишете на C++ - вы тоже молодец, но будет добры, соглашайтесь со всеми правилами языка! Никаких old-style кастов, никакого ручного управления памятью, используйте всё что уже есть в языке грамотно и с умом. И чёрт побери, не забывайте о кросс-платформенности! Код должен быть переносимым ради ваших будущих коллег!
Я это прочитал и подумал: боже, как хорошо, что я в отношении С/С++ остановился на стандарте в районе 1994 года (то бишь после templates но до exceptions), и вообще что сейчас проектирую хардвер на Verilog-е, а не пишу на том ужасе, в который превратился C++.
(При том что я был в прошлом автором продаваемого компилятора с C/C++)
Весьма странная статья. И странные комментарии. Мягкие. Не по мощам елей. На мой взгляд, статья граничит с признанием автора в профнепригодности. Кто то тут способен спутать процедурного и объектного программера? Цирк ёёёё... Не, я конечно понимаю, круг ваших задач ограничен рабочим окружением, но все таки... Ограниченность задач играет с вами дурную шутку дружище. Сложность низкая, объемы кода видимо не высокие, всегда можно прошивку с нуля переписать и не нужно париться с кодом доставшимся в наследство))). А вы возьмите и создайте два варианта даже простенькой игры с GUI на С и на С++ а потом передайте своему коллеге с приколом внести в программу новую фишку. И после этого уже наверное можно вудет с вами обсуждать похожести языков. И все таки настолько не интересоваться программированием, что бы не выйти за рамки рабочей рутины, и не попытаться сделать что то еще, не попытаться получить другой опыт? Я сочувствую той организации где работает автор. Чем похожи С и С++? Кроме синтаксиса и сопоставимого машинного кода на выходе компилятора - они похожи мало чем - буквами. Это два разных инструмента. Они созданы для работы в разных парадигмах и для решения разных задач. И философии программистов их использующих - не сопоставимы ни в кривь ни в кось: почти все что хорошо для С, есть ужас в С++ и наоборот. И эти философии у людей закладываются годами. И именно с инерцией изменения философии человеческой связана сложность освоения новой парадигмы, а не с новым языком. Новый синтаксис ты можешь пробежать глазами и начать использовать за весьма короткое время, но создавать продукт промышленного качества... Оооо - Нет. Как показывает практика, подавляющее большинство работничков не достигает уровня понимания достаточного для самостоятельной работы в принципе. Они так и остаются на уровне кодеров, которым лиды нарезают задачи и контролируют каждый их шаг. Увы.
Возможно я просто чего то не понял. Без обид. Я тоже лох порядочный и не претендую на что то великое. Но. Ты возьми дезассемблированный код, или посмотри машинные коды в PE. что ты там увидишь? Объекты? Процедуры? Не будет их там. Там будут пушенья параметров в стэк и условные/безусловные переходы к адресам. Поэтому, все костыли в виде языков программирования это для нас, для людей. Еще раз повторюсь. У нас разночтение возникло из за разных объемов и наследия. Ничего удивительного нет, что С++ похож на С. С++ выродился, мучительно и тяжело выломался из Си под воздействием обстоятельств, а именно - под давлением возрастающей сложности создаваемых систем. Когда у тебя проект объемом в пол года - год работы коллектива, и ты не имеешь права на ошибку, приходят совсем другие требования. Просто я с Си начинал развлекаться еще в девяностых, потом был С++, потом С#. И в моей голове объектный и процедурный стили никак не смешиваются даже теоретически, как масло и вода, несмотря на обманчивую схожесть синтаксиса и наличие функций/процедур. Я уже давно не смотрю на свои поделки глазами кодера. Как правило, я сначала достаточно долго хожу вокруг новой поделки ковыряяся в носу (I pick in a deep in my big nose), и поковырявшись исторгаю из себя зачатки жизнеспособной архитектуры, обычно это DDD. И тут я никак не могу допустить, что можно коим то образом я могу недопонять где у меня будет линейное решение, а где нет.
"Пушенья и переходы по адресам" это всё же не про парадигму, а про компилятор. Ну и про известную идиому (применимую ко всем подобным языкам) о том, что преждевременная оптимизация - зло. Да, всё превращается в машинные коды конкретного процессора. Но изначально всё идёт из выразительности языка и других плюшек. И хорошо, когда для перехода от объектов к процедурам _не нужно_ менять язык, а можно просто писать в другой парадигме на том же языке.
А вот, например, использование COM (неважно, на С или С++) - в дизассемблере вполне видимо поверх всех "пушений и переходов" (а если есть ещё и отладочные символы - то это вообще очень сильно помогает в reverse engineering). Так что в этом смысле в PE вполне можно увидеть и процедуры, и объекты.
Мне кажется, что на С объёмные проекты вполне удаются. Например ядро Линукс.
Ну или Windows на худой конец.
Кто то тут способен спутать процедурного и объектного программера?
вы наверно не слышали про то, как на плюсах пишут в процедурном стиле или как на си пишут в объектно-ориентированном (пример)? Очевидно что использовать ООП-парадигму в плюсах сильно удобнее чем в си, но и любую другую тоже. В общем, давайте поскромнее со всеми вашими профнепригодностью, ограниченностью, уровнем кодера...
Вот в этом и проблема. Для использования ООП парадигмы надо таки иметь поддержку языка иначе будет очень больно.
Я считаю что использование ООП в pure C это ересь и то что например нагородили в Gnome это ужас. Такое получилось только потому что чистые С-программисты решили доказать миру что на С вполне пишется большая GUI-система. Все что там нагромождено в ООП-стиле - на порядки удобнее и проще и главное безопаснее делать в С++.
Кто то тут способен спутать процедурного и объектного программера?
...
Они созданы для работы в разных парадигмах и для решения разных задач
Так вы не языки сравниваете, а парадигмы. Можно вполне писать на плюсах в процедурном стиле, и это таки будут плюсы, а не си. Он (С++) мультипарадигменный, всё стерпит. Если про С ещё можно сказать, что там сложно в ООП, то вот говорить, что на С++ нельзя в процедуры или даже ФП - уже звучит странно.
Умножаемые комментаторы, а вот такой вопрос:
есть чистый сишный проект, надо сделать что-то где без классов(т.е. без ++) никак от слова совсем.
Насколько "глубоким" проникновение ++ в тело Си проекта допустимо?
Насколько будет правильно писать "до упора" на Си но когда уже никак - то ++, но по самой минималке насколько это оправдано?
А либы не спасут? Основной проект на С, всякие расширения "где без классов никак" - в проект DLL/LIB. Мне кажется более правильным при необходимости до упора следует применять принцип совмещать, но не смешивать ( принцип Бонда ).
При желании, вы можете вынести необходимую функциональность в отдельную либу, написанную на с++, как предложил @dvserg, но здесь начинаются вопросы по ограничениям на линковку? Сможете ли вы линковать свой си-проект со стандартными либами с++ (статически или динамически), если понадобится?
Кроме этого может существовать проблема использования исключений. Будете ли вы их использовать или использовать классы стандартной библиотеки? На некоторых платформах, например, есть ограничения на использование исключений. Если у вас подобный случай, то вам нужно думать, как обрабатывать ошибки классов стандартной библиотеки.
В ядре линукса есть классы и объекты. На чистом С, да.
Может вам не так уж и нужен С++?
Я когда попробовал на с++ пособеседоваться вроде честно поменял все printf на cout <<, и даже насоздавал каких-то классов, но меня всё равно спалили и сказали, что пишу на си. :(
Так что даже выполнение пунктов из статьи недостаточно для качественной мимикрии под плюсовика, там вся парадигма отличается.
std::string не подойдет когда вы не можете работать с динамической памятью (хотя, и тут можно придумать что-нибудь интересное с кастомными аллокаторами);
Очень много чего не подойдет, когда вы не можете работать с динамической памятью. Легче перечислить, что подойдет.
Изначально я думал, что много хейта от того что задели Embedded программистов, как меня. Почитав больше комментариев я вижу что это не так. Задели программистов старой школы си.
Мир шагнул дальше. То что делается на си, за неделю, а раньше и на плюсах делалось за неделю, сейчас на плюсах делается за день. Это прямо не говорится и для Автора очевидно он то знает плюсы, но не очевидно для старой школы. Я то как раз сам прифигел при переходе на новые плюсы (не относится к Qt), и пока не проникнешься - не поймешь.
Автору респект, ты двигаешься в правильном направлении и в правильном двигаешь остальных. Люди, которые вопят про "непрофпригодность" не делали проектов на новых плюсах с учетом всех твоих пунктов и всех нововведений, и им просто непонятно то что тебе уже очевидно. Им кажется, что ты готов выкинуть хорошего программиста на Си (коими все себя считают) только из-за того что "Плюсы лучше". И многие программисты на Си думают что и плюсы тоже самое. Когда-то так и было, но не сейчас, и отличия есть как минимум в ваших пунктах.
Статья хорошая, хоть и при первом прочтении у меня и вызвала негативные эмоции, ибо я тот самый Си-программист и мне обидно признавать, что я сделаю приложение под Linux за больший срок чем даже джун но на плюсах. К счастью, я их и не пишу, а плаваю как раз там где и нужен Си.
Еще раз лучи поддержки Автору
Как мне кажется, программистов на Си сильно больше чем на плюсах, тем более чем хороших программистов на плюсах. И преимуществ у Си много, но они постепенно пропадают. Из реальных областей которые у нас, остались это: малоресурсные мк, низкое потребление или драйвера. Сейчас внутри копеечного мк esp32 памяти столько, что ее даже толком и не считают. За 10 баксов можно взять готовый одноплатник на Linux с более-менее отлаженными драйверами и впихнуть в него даже arm-контейнеры.
Время разработки проектов сильно сократилось за последние 10 лет. Помню пару лет назад читал статью на хабре где посыл был примерно такой:
"Друг попросил сделать освещение по хлопку. Я еб...лся с выбором мк, кое как уместил туда свой код так чтобы работало и выставил другу счет за разработку а он носом повертел. Больше не друг."
При том что уже тогда любой школьник мог просто взять arduino набор за 1000 рублей и все сделать за один вечер скачав библиотеку из интернета, автор той стать упорно все делал с нуля. Уже тогда было понятно что целый класс, невероятно полезных в свое время, людей превратился в набор староверов, мешающих прогрессу и даже теряющих друзей в угоду своей вере. И этот класс страдает от того что их, таких полезных людей, не берут на вакансии С++, хотя казалось бы, каких-то 10 лет назад вообще проблем никаких не было. Страдает, от того что сам понимает что мир шагнул вперед, а они остались на месте. Вы лишний раз напомнили нам об этом и получили вагон минусов.
Вот примерно так думаю я
Мир шагнул дальше. То что делается на си, за неделю, а раньше и на плюсах делалось за неделю, сейчас на плюсах делается за день.
Только если говорить об абстрактных задачах, типа: "на голом языке получи из файла строки, отсортируй их и сохрани в другой файл", потому как в плюсах есть готовые структуры в std.
Если говорить о реальных проектах и о программистах с опытом и багажом библиотек, время будет сопоставимо, а может на C даже меньше, потому как проект собирается за 10 секунд, а не за минуту, потому как при отладке больше всего времени в плюсах уходит на сборку...
В больших проектах большую часть времени занимает не кодирование, а придумывание алгоритмов, отладка и покрытие тестами.
Я долго разрабатывал на C и у меня накопился некислый багаж как сторонних библиотек для решения задач, так и собственных решений под все нужды проектов в моей сфере (системы управления рекламой). И из этого конструктора я собираю решение не медленнее, чем на плюсах, на котором пишу проекты последние лет 5.
Кстати пробовал golang, прекрасный язык, красивый, куча стандартных и сторонних библиотек, но не получилось на нём разрабатывать быстрее, чем на C/C++.
В моем комментарии я не старался охватить все аспекты и позволил себе использовать утрирование. Понятное дело что не каждый проект на плюсах можно сделать быстрее в 5 раз, или даже просто быстрее.
Условия, которые вы составили в своем сообщении можно перефразировать так: "У нас есть набор готовых проверенных решений и библиотек на Си, нам нужно сделать бизнес-логику и запустить в прод. На каком языке писать будем?"
Конечно, в таком идеальном мире ответ очевиден. Представьте что вы пошли в новую область, в которой еще нет библиотек, в которой нужно выдать отлаженное и покрытое тестами решение как можно быстрее, так оно еще должно быть эффективным по производительности. В этом случае "абстрактные задачи" типа "на голом языке и std" обретают реальное воплощение и все чаще встречаются в жизни.
Современные языки, в отличие от Си стараются включать в себя огромный набор стандартных библиотек и это позволяет разным людям работать в одной и той же кодовой базе, а так же быстро разбираться в чужих проектах. Вряд ли всем известно как работать с вашими библиотеками или готовыми решениями. Вы умрете и что делать бизнесу? Новичек облажается с вашими решениями и библиотеками из-за закона дырявых абстракций, но если бы вы пользовались преимущественно std, то вероятность провала была бы сильно ниже.
Я ни в коем случае не намекаю на то что ваш подход не верен. Вы нашли свою нишу, ваш подход в ней эффективен и правилен, бизнес выбирает вас для решения своих задач. Так же как и я, пишу себе код под 8 кБайт оперативки и доволен Си. Автор же говорит о других областях, в которых подход Си устарел
Конечно, в таком идеальном мире ответ очевиден. Представьте что вы пошли в новую область, в которой еще нет библиотек, в которой нужно выдать отлаженное и покрытое тестами решение как можно быстрее, так оно еще должно быть эффективным по производительности. В этом случае "абстрактные задачи" типа "на голом языке и std" обретают реальное воплощение и все чаще встречаются в жизни.
Если не брать новичков и собеседования, обычно программист работает в какой-то "своей" сфере: разработка для микроконтроллеров, gamedev, мобильные приложения и т.п. и работодатель покупает не знание языка и умение решать олимпиадные задачи, а опыт работы в конкретной сфере, а с опытом программист "обрастает" инструментами для решения задач в этой сфере.
Современные языки, в отличие от Си стараются включать в себя огромный набор стандартных библиотек и это позволяет разным людям работать в одной и той же кодовой базе, а так же быстро разбираться в чужих проектах. Вряд ли всем известно как работать с вашими библиотеками или готовыми решениями. Вы умрете и что делать бизнесу? Новичек облажается с вашими решениями и библиотеками из-за закона дырявых абстракций, но если бы вы пользовались преимущественно std, то вероятность провала была бы сильно ниже.
Как показывает мой опыт, для новичков в сфере, к которым вы аппелируете выше ("абстрактные задачи" типа "на голом языке и std" обретают реальное воплощение) фреймоврки значительно затормаживают вхождение в проект, потому как они весьма наворочены и требуют отдельного изучения фреймворка и его осбенностей. Одного языка знать недостаточно.
С другой стороны, хорошо написанный проект, например nginx, написанный на голом C, с собственной реализацией подавляюшего большинства алгоритмов (в том числе деревьев, hash-таблиц и т.п.), читается легко и непринуждённо... На понимание nginx на С мне потребовалось сильно меньше времени, чем на понимание проекта на PHP на Symfony.
Новичек облажается с вашими решениями и библиотеками из-за закона дырявых абстракций, но если бы вы пользовались преимущественно std, то вероятность провала была бы сильно ниже.
В реальном проекте новочёк облажается на уровне архитектуры, а не на уровне использования тех или иных библиотек. По библиотекам есть документация.
Я ни в коем случае не намекаю на то что ваш подход не верен. Вы нашли свою нишу, ваш подход в ней эффективен и правилен, бизнес выбирает вас для решения своих задач. Так же как и я, пишу себе код под 8 кБайт оперативки и доволен Си. Автор же говорит о других областях, в которых подход Си устарел
Я ничего не писал о своём подходе. Автор жалуется на то, что в плюсовые проекты приходят сишники и разрушают его приплюснутый "храм". Но дело тут не в языке, си или плюсах, дело тут в культуре человека, если он не уважает "чужое", он будет одинакого "говнокодить" как на си, так и на плюсах. Меня вот бесит, когда кто-то приходит в проект и начинает писать в своём стиле, а не в стиле, который устоялся в проекте...
Ваше сообщение показывает, что мы с вами очень далеки от понимания друг друга и я не вижу смысла в дальнейшем обсуждении тут, не утверждая прав я или вы. Возможно кто-то следующий подхватит, продолжит и выяснит. Но я, из уважения к вашему небезразличию, оставлю в этом треде этот, последний, комментарий и займусь чем-нибудь еще
Если вы строите стандартные дома-муравейники эконом-класса, тогда да, нужна максимальная унификаци. Если вы строите индивидуальный дом, под требования заказчика, унификации будет меньше.
Проекты бывают разные.
Ну так и используются стандартные конструкции языка и стандартные алгоритмы. Вопрос лишь в том, до какого уровня и зачем используются стандарты.
Если в одном, слабо нагруженном месте я использую std::unordered_map, потому как высокая производительность тут не нужна, то в другом, высоконагруженном месте, я использую JudyArray, потому как он даёт (или давал на момент разработки) более чем двухкратный выигрыш по производительности.
Скажите, пожалуйста, JudyArray -- это стандарт или осебятина от "кулибина"? Если CoCo Hash на моих данных работае лучше контейнеров из std и я его использую -- это стандарт или отсебятина?
Если мне, на каждый запрос надо генерировать uuid и стандартная библиотека это делает это медленно и я пишу собственную, это лютая остебятена и такой код
uuid_t uuid;
uuid_generate_random(uuid);
char cuuid[37];
uuid_unparse_lower(uuid, cuuid);
исключительно лучше понимается проффесионалом, чем такой
rapiduuid::Value uuid;
rapiduuid::Parser parser;
parser.generate(uuid);
std::string s = parser.toString(uuid);
Если только у вас там не совсем что-то оооочень специфическое (например, высокочастотный трейдинг, где все на lock-free построено и хардкорные оптимизации), в чем я сильно сомневаюсь, да и это всё-таки в любом случае редкое явление.
Зря сомневаетесь, не трейдин, но тоже весьма нагружено и по-возможности блокировки не используются.
Проекты разные бывают и нельзя все под одну гребёнку...
Я дорабатывал проект на Java, который писал проффесионал, у которого были все возможные сертификаты по Java, но тупило местали люто.
Управлял проектами на "стандартных" Django, Symfony, где тоже было всё стандартно, но трудно для понимания и оптимизации другими программистами, с хорошим опытом в этих фреймворках.
Были проекты от "кулибиных" с отсебятиной, но читались они как хороший роман. Да вон nginx сплошь отсебятина от "Кулибина".
Не использование стандартов и знание паттернов делает проект хорошим, работающим, понятным.
исключительно лучше понимается проффесионалом, чем такой
Можно задать пару-тройку вопросов, которые сходу возникли у (вроде как профессионала) при первом взгляде на этот фрагмент?
char cuuid[37]; // Почему именно 37?
// Что будет, если написать 36?
// Нижеследующий вызов как-то проверяет
// размерность cuuid? Если нет, то почему?
uuid_unparse_lower(uuid, cuuid); // Что значит "unparse"?
// Почему не "format_to_string_lower"?
// Откуда видно, что результат unparse (что
// бы это ни значило) помещается в cuuid?
Вроде как профессионал может ввести в гугле "uuid_unparse_lower" и понять по первому результату, что это "стандартная" библиотека для генерации uuid в linux и вопрос надо адресовывать авторам библиотеки, а не мне. Не к тому докопались ))
Ну и опять таки, если зайти по этой первой ссылке, то в первом же предложении секции DESCRIPTION есть ответ на ваш вопрос
The uuid_unparse() function converts the supplied UUID uu from the binary representation into a 36-byte string (plus trailing '\0') of the form 1b4e28ba-2fa1-11d2-883f-0016d3cca427 and stores this value in the character string pointed to by out
Вы утверждали, что у профессионала вопросов по коду не возникнут. Вот, возникли. По части из них вы отправляете искать ответы в Интернете, на часть из них не ответили вообще.
Так что приведенный вами код вызывает кучу вопросов, на которые нужно a) тщательно собирать информацию и b) скрупулезно следить за тем, что пишешь, ибо если по недосмотру объявишь cuuid[36], а не cuuid[37], то можно и ногу отстрелить.
C++ в этом плане не сильно далеко ушел, но в нем хотя бы можно от части проблем защититься. В частности, можно сделать совсем тонкую обертку вокруг uuid_unparse:
class uuid_as_cstr {
std::array<char, 37> value_{0};
public:
uuid_as_cstr() = default;
[[nodiscard]] const char * c_str() const noexcept {
return value_.data();
}
[[nodiscard]] char * data() noexcept {
return value_.data();
}
};
[[nodiscard]] uuid_as_cstr
uuid_to_c_str_lowercase(const uuid_t & uuid) {
uuid_as_cstr result;
uuid_unparse_lower(uuid, result.data());
return result;
}
...
const auto cuuid = uuid_to_c_str_lowercase(uuid);
И вы не будете иметь проблем ни с размерностями, ни с тем, что можете случайно начать использовать cuuid до присвоения ему значения.
Вы задаёте вопросы по стандартной библиотеке для решения задачи генерации случайного uuid, которая была приведена в качестве примера, в ответ на утверждение, что стандартное читается проще. Мне странно, что вы от меня ждёте ответов на вопросы, которые должны быть адресованы авторам библиотеки, а не мне. Но ваша реакция говорит о том, что моя "отсебятина" читается лучше, чем стандарт...
Вместо использования 4 строк стандартного кода вы предлагаете написать обёртку вокруг стандартной библиотеки, это должно как-то упростить понимание кода???
Если вы хотели сказать, что в вашем случае uuid получатся одной строкой, а не 4-мя, то в у меня он тоже обёрнут в функцию:
std::string
Utils::makeUUID()
{
uuid_t uuid;
uuid_generate_random(uuid);
char cuuid[37];
uuid_unparse_lower(uuid, cuuid);
return std::string(cuuid);
}
Вы задаёте вопросы по стандартной библиотеке для решения задачи генерации случайного uuid
Я задаю вопросы по примеру кода, который вы продемонстрировали. Причем продемонстрировали именно как пример, к которому вопросы не возникнут.
Мы здесь даже не будем углубляться в выяснение того, в каком именно стандарте определена uuip_unparse и почему она считается "стандартной библиотекой".
Но зато мы можем легко продемонстрировать то, что происходит в реальной жизни: я прихожу в проект, который раньше писался условным Васей Пупкиным, и в первый раз вижу код, вроде показанного вами. И тут у меня возникают вопросы, которые я озвучил. И на которые вы смогли лишь ответить "ищите инфу в интернете". Так ведь это и есть показатель того, что код не так прост в понимании, как может показаться.
Вместо использования 4 строк стандартного кода вы предлагаете написать обёртку вокруг стандартной библиотеки, это должно как-то упростить понимание кода???
Это может выглядеть как парадокс, но тем не менее именно так и происходит зачастую.
Если вы хотели сказать, что в вашем случае uuid получатся одной строкой, а не 4-мя, то в у меня он тоже обёрнут в функцию:
К вашей функции, помимо всех уже заданных вопросов (про размерность cuuid и последствия ошибок с этой размерностью), можно задать и еще один вопрос: а точно ли мы всегда готовы платить за динамическую аллокацию строки из 37-ми символов?
Я задаю вопросы по примеру кода, который вы продемонстрировали. Причем продемонстрировали именно как пример, к которому вопросы не возникнут.
Этот код демонстрировался как "стандартный", а не как тот, к которому не возникнут вопросы.
Но зато мы можем легко продемонстрировать то, что происходит в реальной жизни: я прихожу в проект, который раньше писался условным Васей Пупкиным, и в первый раз вижу код, вроде показанного вами.
Вы его, скорее всего, не увидите, вы увидете что-то типа
id_ = Utils::makeUUID();
и у вас вряд ли возникнут вопросы, что тут происходит и есть ли проблемы в реализации, коль эта функция вызывается десятки миллиардов раз в сутки и за годы проблем не возникало.
Это может выглядеть как парадокс, но тем не менее именно так и происходит зачастую.
Ну каждый верит во что хочет, да.
К вашей функции, помимо всех уже заданных вопросов (про размерность cuuid и последствия ошибок с этой размерностью), можно задать и еще один вопрос: а точно ли мы всегда готовы платить за динамическую аллокацию строки из 37-ми символов?
К любой функции можно при желании докапываться до бесконечности. Конкретно в этом случае временем на аллокацию можно принебречь, потому как в цикле обработки запроса производится на многие порядки больше работы, чем аллокация 37 байт памяти...
Для того места, где это действительно важно, как я писал выше, используется собственная функция генерации случайного uuid, написанная с использованием intrinsic-ов и работающая на 2 порядка быстрее стандартной.
Этот код демонстрировался как "стандартный", а не как тот, к которому не возникнут вопросы.
Обратимся к первоисточнику:
Если мне, на каждый запрос надо генерировать uuid и стандартная библиотека это делает это медленно и я пишу собственную, это лютая остебятена и такой код
...
исключительно лучше понимается проффесионалом, чем такой
Отсюда стороннему наблюдателю (а именно мне) видно, что приведенный вами код демонстрировался именно как тот, который лучше понимается (т.е. к которому вопросов меньше).
Вы его, скорее всего, не увидите, вы увидете что-то типа
Да, я увижу здесь что-то типа C++ и это, знаете ли, как-то вступает в диссонанс с вашим первоначальным комментарием, в котором вы поете хвалебную оду Си и накопленным вами велосипедам.
и у вас вряд ли возникнут вопросы, что тут происходит и есть ли проблемы в реализации
Давайте все-таки обсуждать именно тот пример, который вы привели как "исключительно лучше понимается". Если же вы сами убедились, что пример не показателен, то приведите другой пример, который мог бы проиллюстрировать вашу мысль лучше.
Любимый момент:
Использует char*-строки и функции <string.h> вместо std::string и std::string_view. (единственное исключение - строковые константы через constexpr).
А это хорошо или плохо? String упрощает взаимодействие со строками, но char* и char str[] дает возможность строго определить занимаемое место. Работаю в основном с программированием микроконтроллеров, и неопределенность, которую вносит string, недопустима, и его стараюсь избегать, используя там, где действительно нужно.
Например, при определении структуры, которая будет передаваться и использоваться в очередях FreeRTOS, string уже не прокатит: недетерминированное поведение строки при передаче через очередь
А какая польза от static_cast<int>(number) по сравнению с (int)number?
Использование static_cast - это проверка допустимости преобразования во время компиляции.
Это понятно. Но какая польза от static_cast<int>(number)
по сравнению с (int)number
в случае, когда number
— это простое выражение с очевидным типом? Очевидный вред от static_cast
— это многословность.
Одно дело, когда мы работаем с шаблонами, и тогда это всё оправдано. И совсем другое — это что-то вроде:
auto N = (int)ceil(p / 2.0)
Так там же написано "number". Если бы "непонятно что" или "указатель", то другое дело.
Евангелисты C++ утверждают, что надо использовать static_cast всегда и везде, даже в очевидных случаях.
При этом, если почитать посты от PVS Studio с разбором кода проектов, то окажется, что ошибки, связанные с неправильным приведением типов, когда мог бы помочь static_cast, встречаются чуть более, чем никогда.
Спор между "так символов меньше" + "программисту очевидно" + "незачем усложнять язык" и "строгая типизация лучше слабой" + "больше проверок компиляции лучше" + "код гораздо больше читается, чем пишется" можно продолжать бесконечно.
Я вот во втором лагере. Данный пример вижу так:
Если вы не используете венгерскую нотацию в том или ином виде, то там будет не number
, а какое-нибудь другое название.
Например, есть ли ошибка в следующем коде, если он компилируется и не падает?
long long timeout_min =
((long long)read_config("timeout_sec") + 59) / 60;
Ответ: а фиг его знает.
Если там возвращается double
(потому что в JSON только вещественные числа) — то, наверное, тут хотели округлить секунды вверх до минут. А вот если там случайно возвращается сишная строка, то вы только что взяли в качестве количества секунд значение указателя. И никаких предупреждений компилятора не получили даже на самом высоком уровне предупреждений.
Разумеется, если протестируете, что таймаут у вас корректно работает, вы сразу обнаружите ошибку. Если протестируете. Или хотя бы выведете на экран.
Но тогда, мне кажется, почём зря приближаемся к другому краю спектра с полным отключением статического анализа типов, как в Python. Да, можно код протестировать, но мне больше нравится получать ошибки компиляции, даже если приходится больше набирать или больше думать.
с полным отключением статического анализа типов, как в Python
Я сейчас пишу на Пайтоне со включенным статическим анализатором. Боже, как это прекрасно. Я уже забыл что такое когда ты запускаешь программу и она тут же сразу работает.
Совершенно недостаточно.
Я тут пытался написать своего рода "шаблонную" функцию которая принимает тип и потом возвращает значение этого типа. Так и не смог донести до питоновского typing что же я хочу. Т.е. функцию я то написал, но заставить питон выводить правильный тип функции не смог.
Но все равно, type annotations - это большой шаг вперед. Я вообще не профессиональный python разработчик, так иногда пишу тулзы разной степени сложности. И даже в мелких скриптах type annotations сильно облегчают жизнь.
Кажется, должны помочь type variables/generics? Например:
from typing import TypeVar
T = TypeVar('T')
def foo(a: T) -> T:
return a
x: int = foo(20) # ok
y: str = foo('x') # ok
z: str = foo(20) # incompatible types in assignment
Это не совсем то что я хотел:
def get_typed_value(obj: MySpecialObj, T: type) -> Optional[T]:
# Perform some checks on "obj"..
# ..
# And then
if isinstance(obj.value, T):
return obj.value
return None
Так, конечно же вообще не работает. А хотелось бы.
Работает вот такой вариант:
def get_typed_value(obj: MySpecialObj, typename: type) -> Optional[T]:
Но тайпчекер не может вывести правильный тип для вызова
`get_typed_value(o, int)` например.
Вообще я хотел пойти дальше и сделать пачку хелперов вот таким образом:
get_int_value = partial(get_typed_value, typename=int)
get_boolean_value = partial(get_typed_value, typename=bool)
и т.д.
Но опять же, тайпчекер не может вывести правильный тип Callable[[MySpecialObj], int] для таких функций (и это понятно, потому что на самом деле partial возвращает не Callable).
Короче, такие приколы возможны в каком-нибудь Хаскеле, но явно не в питоне. А жаль.
В чистом Haskell, мне кажется, нельзя, там типы нельзя передавать в качестве аргументов функций. Или получаем зависимые типы (dependent types) и какую-нибудь Agda.
Ну да, я пожалуй неправильно выразился. В Haskell можно использовать какое-то подобие run-time polymorphism, но оно мягко говоря не идиоматично, насколько я понимаю. Поэтому никаких isinsrance()
. И та проблема, которую я решал (чуть-чуть более продвинутая десериализация YAML) решается совсем по-другому. Похожий кусок кода там просто в принципе не возник бы.
В соседнем комментарии привели пример:
long long timeout_min =
((long long)read_config("timeout_sec") + 59) / 60;
Код компилируется, но в нем ошибка, если read_config возвращает, например, указатель.
Что делать? Можно исправить код и счастливо пойти дальше собирать те же грабли. А можно что-то придумать, чтобы избежать таких ошибок в дальнейшем.
Что придумать? Например, не разрешать приводить что угодно к чему угодно. Для этого придумали семейство операций: static_cast, dynamic_cast, reinterpret_cast, const_cast. Каждая из них разрешает только некоторые виды приведений типов.
В приведенном примере кода static_cast не разрешит преобразование указателя к целочисленному типу и приведет к ошибке компиляции.
Но вообще говоря, это костыли, борьба с симптомами болезни, а не с самой болезнью. Приведений типов в коде лучше, по возможности, избегать. ЕМНИП поэтому для операций приведения выбрали такие уродливые длинные названия — чтобы мотивировать разработчиков от этих операций избавляться, переписав код (но конечно ни в коем случае не возвращаться обратно к сишному приведению!). Если же по каким-то причинам приведение типа действительно необходимо, то опять хорошо — уродливый синтаксис поможет не забывать об этом опасном месте в коде. Не лишним будет и написать объяснительную в комментарии, что это за место такое, и почему красиво сделать не получилось.
Почему нигде нет варианта "пишу на Си с классами, потому что в проекте огромная команда, и такой код проще всего развивать и поддерживать"?
Посмотрите в код Open Source проектов - сплошь и рядом Си с классами. Написали на махровых плюсах - половина контрибьюторов минус.
У меня 3 вопроса:
1 Чем #define хуже const при объявлении константы? Насколько я помню он же фактически заменяет одну последовательность символов другой на каком-то из начальных этапов компиляции.
2 А можно поточнее, о каком разделе эмбедеред идёт речь? Точнее на кой нам плюсы даже в 32-битном контроллере для стиралки при том, что на нём можно даже простенькие гуи для сенсорного дисплея писать?
3 И совсем уж нубовский вопрос: какое такое сакральное преимущество даёт C++ системном программировании что бы это нельзя было сделать на си? Ядра ж в конце концов на нём пишут. Речь не идёт о ситуации когда вам проще поставить какой нибудь линукс в свой контроллер и писать уже обычные программы.
> И совсем уж нубовский вопрос: какое такое сакральное преимущество даёт C++ системном программировании что бы это нельзя было сделать на си?
Ни один язык не даёт сакральных преимуществ над другим. Даже если (что не так в случае Си и C++) является строгим надмножеством другого; не всегда больше возможностей — это хорошо.
А конкретная команды в конкретном проекте делает выбор, исходя из ситуации. Язык, тулчейн, стиль кода, архитектура...
Сакральное преимущество очень простое: Автоматический вызов деструкторов.
Автоматический вызов деструкторов может приводить к нежелательным эффектам. Например, есть следующие стандартные аргументы (мне, впрочем, автоматический вызов всё равно нравится больше альтернатив):
За
}
теперь может скрываться сколь угодно сложная операция, работающее произвольное время и потребляющая произвольное количество ресурсов. Обычно, конечно, небольшое, но может и взорваться. Это же, кстати, стандартный аргумент против перегрузки операторов (с которым тоже можно вести холивары).В частности, если мы пишем какое-нибудь владеющее дерево/список, то при наивной реализации (через
unique_ptr
и сгенерированные деструкторы) для его уничтожения требуется стек размера с глубину этого дерева. Можно случайно переполнить стек и даже не догадаться: рекурсии-то мы нигде явно не писали, она скрыта.Деструкторы провоцируют писать логику для закрытия/сброса буферов именно в деструкторах. Создаётся ложное ощущение, что это безопасно, хотя на самом деле ошибки при закрытии (диск отвалился => не удалось файл записать) тут либо полностью игнорируются, либо приводят к падению приложения. Если же, например, есть явный вызов
close()
, который надо делать абсолютно всегда (либо под угрозой назальных демонов, либо чего-нибудь попроще), то за его наличием следить проще и привычнее.
Верно, я уже наобжигался с RAII, когда писал многопоточный код. Программа уходит в дедлок. А почему? А потому что логика скрыта в деструкторах, отлаживать которые — дело неблагодарное. С тех пор на апологетов RAII смотрю с большим недовольствием, ресурсы чищу вручную, а деструкторы рассматриваю как обработку аварийных сценариев.
Так разговор шёл именно об "сакральном" отличии С от С++. Почти всё из С++ можно реализовать средствами языка С, но вот это -- к сожалению нет.
И всё, что вы описали, согласитесь, может произойти и при ручном удалении.
Преимущество деструкторов в другом -- на каждый return или "}" не нужно вычислять что ты уже освободил а что ещё нет.
Особенно, если нужно добавить что то в уже существующий код, с множеством управляемых этим кодом ресурсов.
Насколько я помню он же фактически заменяет одну последовательность символов другой на каком-то из начальных этапов компиляции.
Ровно этим и хуже (помимо указанного выше). По крайней мере, пока не привыкнуть обходить его острые углы. Вы хотите константу объявить, а не замену символов произвести. Классический пример из олимпиадного программирования:
#define MAXN (int)1e6 + 1
int data[2 * MAXN]; // int data[2 * (int)1e6 + 1];
Хотели объявить массив размера два миллиона и два элемента, а получили массив размера два миллиона и один элемент.
Чинится легко — добавляем вокруг каждой такой макроконстанты круглые скобки.
А дальше выбираем в своём проекте: ничего с этим не делать, забанить #define
для констант, разрешить и форсить наличие круглых скобок (только где обязательны или вообще везде — отдельный вопрос) на code review/автоматически/не форсить вообще и разгребать редкие последствия.
Конечно, если много раз писал #define
, то скобки наверняка на автомате ставишь. Вот и холивар: кому-то это вообще риском не кажется, просто "надо уметь писать", а кому-то риском кажется. Потом скатываемся в стандартный спор на тему "нужны ли code style", "какой code style использовать"...
Пожалуй malloc/free - самый явный C-прогер детектор ИМХО)
Есть несколько возражений:
1) Код соответствует стандарту С++, если он компилируется компилятором С++. Шах и мат. Между прочим Бьярне ровно так и говорит - идите нахер, пуристы.
2) В общем легко отличить программиста на С++ от программиста С даже без этих пунктов. Если есть код, который можно поглядеть (и доказуемо, что его автор перед вами), то там очевидно все будет. Дальше уже могут быть тонкости, например "этот автор как-то плохо разбирается в поведении string_view".
3) Кажется лучше взять хорошего программиста на С и переучить его на нужный диалект С++, чем упарываться в пуризм.
Код соответствует стандарту С++, если он компилируется компилятором С++.
Каким из компиляторов? Они, бывает, отличаются и добавляют свои собственные расширения. А ещё поведение отличается между ОС, потому что помимо компилятора есть стандартная библиотека и протекающие абстракции.
Ill-Formed No Diagnostics Required куда делся?
Код соответствует стандарту С++, если он компилируется компилятором С++
А что, в стандарте С++ убрали undefined behavior?
Узнал массу интересного про С++. Спасибо, очень полезно. Видимо, меня уже поздно переучивать и я буду и дальше работать в С++, главным образом, Си-шными методами.
свои пять коп: приплюснутый компайлер считает себя умнее программера, а сишный компайлер считает программера умнее себя. to whom how. ну или ССЗБ.
- std::string не подойдет когда вы не можете работать с динамической памятью (хотя, и тут можно придумать что-нибудь интересное с кастомными аллокаторами);
Так можно просто взять std::pmr::string
из стандартной библиотеки. Или это как раз и подразумевается под "кастомными аллокаторами"?
9/19 "за", так и знал, что я не плюсовик! :)
А ещё забыли:
"Использует #ifndef MY_SOMETHING #define MY_SOMETHING #endif вместо #pragma once",
"Использует битовые поля вместо std::bitset."
"Использует сишные библиотеки на прямую без уровня абстракции над ней"
"В заголовочных файлах куча инклудов, которые можно было в принципе там и не писать (incomplete class)"
Использует ручное управление памятью с new и delete, вместо RAII и умных указателей;
Я конечно извиняюсь, но new&delete с самого начала были именно что с++ фишки
Для деструкторов забывает virtual :)
А у вас постоянно встречается ромбовидное наследование, что ли? Это же фу-фу
Я конечно извиняюсь, но new&delete с самого начала были именно что с++ фишки
Однако, в современных плюсах их использование есть моветон, а сишникам наверняка проще привыкнуть писать new/delete вместо malloc/free, чем освоить концепцию RAII.
А у вас постоянно встречается ромбовидное наследование, что ли? Это же фу-фу
Причём здесь ромбовидное наследование? Отсутствие виртуального деструктора в любом интерфейсном (абстрактном) классе — это выстрел себе в ногу.
Однако, в современных плюсах их использование есть моветон, а сишникам наверняка проще привыкнуть писать new/delete вместо malloc/free, чем освоить концепцию RAII.
new/delete - это чисто с++ концепции (вызов конструктора/деструктора при аллокации/очистке памяти), к С не имеют никакого отношения
Причём здесь ромбовидное наследование? Отсутствие виртуального деструктора в любом интерфейсном (абстрактном) классе — это выстрел себе в ногу.
[вопрос снимается, ниже ответили]
Поэтому использование new и delete часто является одним из признаков того, что человек пришел из языка, где нет умных указателей и подобных оберток и не привык к ним, либо вообще не подозревает о их существовании.
я уже давно живу в мире, где есть garbage collectors. Могу много рассказать про преимущества ручного управления памятью в частных случаях
Имхо, не-пользование unique_ptr это скорее про незнание отдельных аспектов стандартной библиотеки, а не про понимание принципов ооп
без virtual при delete вызовется только деструктор парента, но не производного, и вы можете получить утечку ресурсов.
ясно
ужас какой, как хорошо, что я на плюсах уже почти и не пишу; забыл уже все эти заморочки
К принципам ООП, впрочем, умные указатели отношения имеют мало (разве что в плане самих понятий "конструктор" и "деструктор", на которые они полагаются). Умные указатели можно встретить даже там где вообще нет ООП, например в проектах на чисто процедурном Си
тогда посыл статьи не совсем понятен
она тогда всего лишь о том, что новонанятый прогер просто должен отлично знать современную std
Но это ж несерьезно. Умения программиста они не совсем о знании фреймворков
я уже давно живу в мире, где есть garbage collectors. Могу много рассказать про преимущества ручного управления памятью в частных случаях
GC и smart pointers — очень разные концепции, и последние покрывают большинство сценариев, где GC уступает ручному управлению.
Имхо, не-пользование unique_ptr это скорее про незнание отдельных аспектов стандартной библиотеки, а не про понимание принципов ооп
Это действительно не имеет никакого отношения к ООП. Это про RAII — гораздо более важную и центральную особенность C++. Не понимать и не использовать RAII == не знать плюсы.
без virtual при delete вызовется только деструктор парента, но не производного, и вы можете получить утечку ресурсов.
Здесь на самом деле всё ещё хуже, такой delete вообще является UB, и, например, в моей практике приводил к memory corruption, после чего любая аллокация могла с некоторым шансом вызвать дедлок.
GC и smart pointers — очень разные концепции, и последние покрывают большинство сценариев, где GC уступает ручному управлению.
идею-то одна - автоматическое управление памятью
Да и в тех сценариях, где нужно ручное управление, умные указатели будут такими же (если не более) неуклюжими, как и gc
Будем откровенны, смарт поинтеры - это все таки костыль в сравнении в нормальным gc (вот который в дотнете, например)
Это действительно не имеет никакого отношения к ООП. Это про RAII — гораздо более важную и центральную особенность C++. Не понимать и не использовать RAII == не знать плюсы.
всегда думал, что плюсы это больше про ООП, а не про способы управления ресурцами
Есть много случаев, когда GC неуместен вообще, например когда у нас real-time или ограничены ресурсы.
насколько мне известно, в таких случаях будет использоваться скорее с, а не плюсы:))
Просто из-за того, что многие GC явно делают stop-the-world, а если даже и не делают, то накладные расходы на его работу все равно явно ненулевые.
конечно
как и на все фишечки std. Это стандартный трейдоф удобство вс перформанс; но чаще-то всего железо позволяет
помню, один товарищ (игродел) рассказывал, что они даже виртуальные функции стараются не использовать в борьбе за fps - переход по vtable ест пару тактов:) Но это давно еще было
Плюсцы это мультипарадигменный язык, и даже при написании кода в чисто процедурном стиле на плюсах вместо голых сей, можно получить немало полезных фишечек и удобств.
он адски сложный. Сколько там стандарт уже, полторы тыщи страниц? Честно, не вижу особого смысла его ни вспоминать, ни учить.
При этом, взять хотя бы вот это обсуждение - тут 80% дискуссии о том, как правильно использовать фишки языка. Не как правильно определить бизнес логику/архитектуру/алгоритмы, а как правилно использовать язык. Такая вещь в себе этот ваш с++
Причем, мне сейчас за этот камент еще и минусов насуют, очень нетерпимая коммьюнити:))
Я уже с десяток лет с подобным работаю. Именно на плюсах. И таких проектов все больше и больше.
а на Go или там Rust народ не хочет переходить? Не помню, какой там из них в нативный код компилится
А вот на C++ по ряду причин такое сделать получится легко, и как показывает практика, очень многие пытаются так делать. Об этом и статья.
это понятно, я все еще помню жаркие дискуссии с vs c++ в начале этак 2000х. "Веревка достаточной длины, чтоб выстрелить себе в ногу", а как же:)
Но это, имхо, не достоинство прикладного языка
При этом, взять хотя бы вот это обсуждение — тут 80% дискуссии о том, как правильно использовать фишки языка. Не как правильно определить бизнес логику/архитектуру/алгоритмы, а как правилно использовать язык.
А что ещё вы ожидали от комментариев к статье, поднимающей вопрос идеоматичного кода на C++?
так статья, по сути, не об умении программировать, а о знании некоторых специфических (пусть и широко распространенных) особенностей фреймворка (std)
Мне такое странно, фреймворк и фреймворк, нормальный прогер основные положения выучит за пару месяцев, это не рокет саенс. А тут прям такое внимание
Статья о сравнении концепций, которые принято применять в C и C++. Да, часть из них реализуется с помощью абстракций, которые не входят в сам язык, но 1) от программиста на C++ обычно ожидается хотя бы поверхностное знакомство с STL 2) другие фреймворки в большинстве своём всё равно либо опираются на STL, либо предлагают аналогичные абстракции — использовать сишные строки или вручную управлять памятью вы наверняка не будете, независимо от фреймворка.
идею-то одна - автоматическое управление памятью
в случае с умными указателями - не только памятью, а объектами в общем, пояснение ниже
Да и в тех сценариях, где нужно ручное управление, умные указатели будут такими же (если не более) неуклюжими, как и gc
Будем откровенны, смарт поинтеры - это все таки костыль в сравнении в нормальным gc (вот который в дотнете, например)
давайте для начала вспомним что в сравнении с GC умные указатели не вносят оверхеда, то есть уже применимы в большем числе юзкейсов. Во-вторых, сам по себе GC не дает абсолютно ничего в сравнении с умными указателями, кроме некоторого снижения когнитивной нагрузки, т.е. недостаточная выразительность языка компенсируется за счет доп. расходов рантайма.
А ручное управление памятью в языке с GC это костыль для бекдора в другом костыле...
всегда думал, что плюсы это больше про ООП, а не про способы управления ресурцами
а что такое ООП по вашему? Из трех "краугольных камней" ООП основополагающей является именно инкапсуляция. Мы объединяем логику, данные и ресурсы, в некоторую единую абстракцию - объект. RAII позволяет делать герметичные объекты, которые при уничтожении освобождают все ресурсы. Иначе могут получаться "протекающие" абстракции - объект уничтожен, а ассоциированный с ним ресурс - нет. Что собственно и происходит в языках с GC.
ужас какой, как хорошо, что я на плюсах уже почти и не пишу; забыл уже все эти заморочки
учитывая степень вашего понимания базовых концепций программирования хорошо конечно, что вы не пишете на с++...
в случае с умными указателями - не только памятью, а объектами в общем, пояснение ниже
это без разницы, обьекты расположены в памяти. Ну, разве что вы имеете в виду некие ресурсы на диске, обьекты ядра и проч. Но суть все равно та же - автоматически освобождать ресурсы, когда они не нужны
Во-вторых, сам по себе GC не дает абсолютно ничего в сравнении с умными указателями, кроме некоторого снижения когнитивной нагрузки
он дает чудовищное снижение этой самой нагрузки
Попишите, например, год на сишарпе или там жаве, и потом опять на плюсах - разницу сразу прочувствуете.
т.е. недостаточная выразительность языка компенсируется за счет доп. расходов рантайма.
это не к языку, это к CLR/virtual machine
Что касается рантайма - разница там не сказать чтоб значительная, к тому же выделение памяти в managed heap сильно быстрее, чем в нативной, в силу отсутствия дефрагментации; да и современные gc нонеча работают уже совсем не так как давеча
а что такое ООП по вашему? Из трех "краугольных камней" ООП основополагающей является именно инкапсуляция. Мы объединяем логику, данные и ресурсы, в некоторую единую абстракцию - объект
одно из самых странных обьяснений ООП что я слышал, если честно
противоречит хотя бы тому же single responsibility
Иначе могут получаться "протекающие" абстракции - объект уничтожен, а ассоциированный с ним ресурс - нет. Что собственно и происходит в языках с GC.
не, не происходит:) Ну разве что с кастомными плохо спроектированными обьектами, использующими unmanaged resources
учитывая степень вашего понимания базовых концепций программирования хорошо конечно, что вы не пишете на с++...
каким образом понимание программирования на с++ связано с пониманием программирования вообще? Имхо, тут скорее наоборот, знание с++ требует столько умственных усилий, что на собственно программирование их и не остается
В том же Шарпе это будет не "когда они не нужны", а "когда они не нужны и до них наконец-то доберется сборщик мусора". Поэтому в языке пришлось изобретать костыли с using.
ну что вы, давным давно уже не так
запустится он, когда у него перестанет хватать памяти под новые обьекты, при этом, все обьекты он проверять на удаление не будет. А пока ее хватает, какие проблемы
Была тут вроде переводная статья про gc как эмулятор бесконечной памяти
что до using - это не про чистку памяти, это про гарантированный вызов некоей закрывающей функции, например, чтоб файл закрыть, или там курсор по базе
IDisposable никоим образом не гарантирует освобождение ресурсов, это просто интерфейс
такой аналог try/finally
ну да, там поколения еще есть, если я правильно помню.
там много чего есть:)
Это я к тому, что в плюсах с этим проще, там RAII с умными указателями подходят и используются для обоих случаев - и для освобождения ресурсов, и для автоматического вызова закрывающей функции
а если мне закрыть надо, а ресурс освобождать нет?:) Они ж довольно примитивные, unique_ptr эти. Никто ж не строит графов обьектов и не проверяет ссылки на них; вызвал кто reset, и все. А если на указатель внутрях еще один unique ссылается?
Ну и вроде все равно надо явно вызывать что-то типа unique_ptr.reset(nullptr), нет?
а если мне закрыть надо, а ресурс освобождать нет?
То вероятно это должны быть два разных уровня абстракции.
Никто ж не строит графов обьектов и не проверяет ссылки на них; вызвал кто reset, и все. А если на указатель внутрях еще один unique ссылается?
unique_ptr
называется unique не просто так, два unique_ptr
не могут ссылаться на один объект. Для таких случаев существует shared_ptr
, который ведёт счётчик ссылок и удаляет объект только вместе с последней ссылкой.
Ну и вроде все равно надо явно вызывать что-то типа unique_ptr.reset(nullptr), нет?
Нет, обычно полагаются на вызов деструктора при выходе из скоупа. Вызов unique_ptr::reset()
практически полностью аналогичен delete
и существует для тех ситуаций, когда вам всё-таки необходимо ручное управление ресурсами.
unique_ptr называется unique не просто так, два unique_ptr не могут ссылаться на один объект.
в документации ничего про это не сказано. Это отдано на откуп программисту, что ли? Ну так это и есть та самая веревка, которой можно выстрелить себе в ногу
Нет, обычно полагаются на вызов деструктора при выходе из скоупа
Понятно
Тогда мы снова возвращаемся к ситуации, когда мне (в случае необходимости немедленного освобождения памяти) нужно писать ручное удаление, которое еще и черти к чему приведет
О чем я и говорю, очень примитивная обертка, не гарантирующая, по сути, ни от чего
Прямым текстом сказано вообще-то:
дык а как это обеспечивается на уровне реализации этого UP? что мне мешает сделать указатель на обьект при помощи new и запихнуть его в 2 разных UP? Тока не говорите, что здравый смысл - это а) как повезет б) в сложных codebases вручную все проконтролировать невозможно
Объявили объект внутри блока {} - он удалится когда закончится этот блок
я, может, неправильно все делаю, но что-то вот в таком раскладе деструктор у меня не вызывается
class AutoCheck
{
public:
AutoCheck(int i)
{
inner = i;
}
~AutoCheck()
{
inner = 0;
}
private:
int inner;
};
int main()
{
auto test = new AutoCheck(10);
{
auto p = std::make_unique<AutoCheck*>(test);
auto p1 = std::make_unique<AutoCheck*>(test);
}
}
похоже, мы всю дорогу о разном говорили.
unique_ptr тут вообще сбоку, говорить надо было про auto_ptr
в документации ничего про это не сказано. Это отдано на откуп программисту, что ли? Ну так это и есть та самая веревка, которой можно выстрелить себе в ногу
У него просто нет копирующего конструктора / присваивания, только перемещающие.
Выстрелить себе в ногу можно разве что хитро извернувшись в стиле unique_ptr{other_ptr.get()}
.
Тогда мы снова возвращаемся к ситуации, когда мне (в случае необходимости немедленного освобождения памяти) нужно писать ручное удаление, которое еще и черти к чему приведет
Скоуп — любой блок кода в фигурных скобках, если ресурс должен быть занят только при выполнении нескольких строчек кода, которые не хочется выносить в функцию, то эти строки просто оборачиваются в фигурные скобки.
Обычная практика в плюсах для блокировки мутекса, например.
У него просто нет копирующего конструктора
auto test = new int(10);
auto p = std::make_unique<int*>(test);
auto p1 = std::make_unique<int*>(test);
а так?
Тока не говорите, что здравый смысл - это а) как повезет б) в сложных codebases вручную все проконтролировать невозможно
привести искуственный пример самострела можно в любом языке. И точно так же невозможно вручную проконтроллировать отсутствие его в достаточно большой кодобазе. Ну и не просто так рекомендуется использовать make_unique.
Что до здравого смысла - он ведь не дает вам например выпрыгнуть из окна, верно?
а так?
вы видите тут вызов копирующего конструктора?
привести искуственный пример самострела можно в любом языке. И точно так же невозможно вручную проконтроллировать отсутствие его в достаточно большой кодобазе. Ну и не просто так рекомендуется использовать make_unique.
я о том, что в силу особенностей языка использование unique_ptr на спасет ни от чего, выстрелить в ногу можно точно так же, как и 20 лет назад
просто тогда б-м прошаренный народ писал свои обертки и использовал всякие creator patterns
а вот наличие gc от такого вполне так себе гарантируeт
Использование unique_ptr защищает об большинства ошибок по невнимательности, но не защитит, если вы упорно намеренно и осознанно пытаетесь выстрелить себе в голову.
или использую его неправильно, от чего язык не защищает никак
Я, собственно, об этом в основном. Т.е., язык практически не изменился, просто оброс некими обертками, кои можно использовать правильно, можно неправильно, а можно и вообще не использовать.
Это мне напоминает одну контору в конце 90х, в которой было запрещено использовать арифметические операции с указателями - чтоб, зчачить, не попортить чего
А что вы этим кодом хотели показать? Что у типа int *
есть конструктор копирования? Ну так не используйте указатели, и будет вам счастье.
Нормальный человек же напишет:
auto test = std::make_unique<int>(10);
После чего закономерно словит ошибку компиляции.
не словит
Конечно же словит. Нельзя привести std::unique_ptr<int>
к int*
.
Можно написать так, выстрелив в ногу через .get()
:
auto test = std::make_unique<int>(10);
auto p = std::make_unique<int*>(test.get());
auto p1 = std::make_unique<int*>(test.get());
А можно написать так:
auto test = std::make_unique<int>(10);
auto p = std::make_unique<std::unique_ptr<int>>(std::move(test));
auto p1 = std::make_unique<std::unique_ptr<int>>(std::move(test));
Никаких проблем, кстати, со вторым кодом не будет. Но это просто дичь.
Стоило бы сначала прочитать документацию make_unique
и unique_ptr
, ваш код эквивалентен
int* test = new int(10);
int** p = new int*(test);
int** p1 = new int*(test);
delete p1;
delete p;
Скорее всего вы имели в виду
auto test = new int(10);
std::unique_ptr<int> p(test);
std::unique_ptr<int> p1(test);
А я могу привести ещё один пример:
struct RecVector : std::vector<RecVector> {
using std::vector<RecVector>::vector;
};
И сделав
RecVector a;
a.push_back(a);
Вы получите утечку памяти.
Знаете, чем этот пример отличается от вашего?
Тем, что я готов поверить в то, что что-то подобное, хоть и маловероятно, но может случайно возникнуть в большом и сложном проекте.
А само наличие new
совершенно очевидно указывает на проблемное место, и ни один плюсовик, знакомый с хорошими практиками, такую ошибку никогда не допустит, и через кодревью не пропустит.
В C++ есть множество способов выстрелить себе в ногу, но не надо приплетать к ним случаи, возможные только в искуственных примерах.
А само наличие new совершенно очевидно указывает на проблемное место, и ни один плюсовик, знакомый с хорошими практиками, такую ошибку никогда не допустит, и через кодревью не пропустит.
ну т.е. в языке есть задокументированная в стандарте конструкция, пользоваться которой нельзя:) Понятно:)
Скорее всего вы имели в виду
да я уже посмотрел внутрь make_unique, вижу, что он указатель на переданное value создает
А как быть с созданием картинок, например, в цикле? Создал-обработал-автоудалил? Мне штож, в стеке их создавать?
В C++ есть множество способов выстрелить себе в ногу, но не надо приплетать к ним случаи, возможные только в искуственных примерах.
как-то, говорю ж, с 98 года не сильно все изменилось
Можно, если вам это действительно нужно и вы понимаете, что и зачем вы делаете.
если судить по исходной статье и обсуждению, такого знающего просто на работу не возьмут:))
А в чем сложность? Сделали make_unique внутри цикла, он автоматически удалит созданную картинку при завершении итерации.
вопрос, что мне передавать в пареметр make_unique? Картинку (например, в полгига), созданную на стеке?
Изменилось все настолько сильно, что я бы сказал, что C++98 и C++17 - это вообще почти что два разных языка
эт не язык изменился, это стандартная библиотека изменилась
вопрос, что мне передавать в пареметр make_unique? Картинку (например, в полгига), созданную на стеке?
Может, наконец, прочитаете документацию?
make_unique переадресует аргументы в конструктор типа, его суть ровно в том, что вам не нужно сначала создавать объект, а потом помещать в unique_ptr, как приходилось делать до c++14.
эт не язык изменился, это стандартная библиотека изменилась
Обсуждаемые нововведения были бы малополезны без появившегося в c++11 move semantics.
это без разницы, обьекты расположены в памяти. Ну, разве что вы имеете в виду некие ресурсы на диске, обьекты ядра и проч. Но суть все равно та же - автоматически освобождать ресурсы, когда они не нужны
файловые дескрипторы, сокеты, мьютексы в конце концов. RAII дает куда больше чем простое освобождение памяти.
он дает чудовищное снижение этой самой нагрузки
Попишите, например, год на сишарпе или там жаве, и потом опять на плюсах - разницу сразу прочувствуете.
вы сильно недооцениваете моё понимание плюсов и сильно переоцениваете моё понимание джавы/шарпа.
это не к языку, это к CLR/virtual machine
по факту они скорее нераздельны
Что касается рантайма - разница там не сказать чтоб значительная, к тому же выделение памяти в managed heap сильно быстрее, чем в нативной, в силу отсутствия дефрагментации; да и современные gc нонеча работают уже совсем не так как давеча
Единственный вариант когда managed heap теоретически может быть быстрее - если специфика нагрузки хорошо ложится на конкретный аллокатор. Опять же, такие сценарии в плюсах покрываются переопределением аллокатора. В противном случае GC всегда будет медленнее, а идеальный GC по сути сводится к автоматическому управлению. Подробнее можете ознакомиться в этом докладе
одно из самых странных обьяснений ООП что я слышал, если честно
противоречит хотя бы тому же single responsibility
Во-первых, вы смешиваете категории. Парадигма ООП основана на инкапсуляции, а принцип единственной ответственности - всего лишь паттерн ООП. Во-вторых, здесь нет никакого противоречия. Простой пример: класс File обычно будет инкапсулировать файловый дескриптор (ресурс), данные (путь до файла, состояние, режим открытия), ну и соответственно логику работы с ним (методы открыть/закрыть/читать/писать). При этом SRP не нарушается, ведь File всё еще выполняет ровно одну задачу.
каким образом понимание программирования на с++ связано с пониманием программирования вообще? Имхо, тут скорее наоборот, знание с++ требует столько умственных усилий, что на собственно программирование их и не остается
я критикую ваше понимание ООП а не с++. А еще вы опять смешиваете категории, на этот раз "я не справился с с++" и "с++ не нужен"
запустится он, когда у него перестанет хватать памяти под новые обьекты, при этом, все обьекты он проверять на удаление не будет. А пока ее хватает, какие проблемы
действительно, какие проблемы выжрать 300 гигов памяти на серваке, а потом потратить десяток секунд на её очистку, пока все остальные приложения ждут...
что до using - это не про чистку памяти, это про гарантированный вызов некоей закрывающей функции, например, чтоб файл закрыть, или там курсор по базе
IDisposable никоим образом не гарантирует освобождение ресурсов, это просто интерфейс
собственно в этом и проблема. Вы разве никогда не сталкивались с проблемами типа заканчивающихся файловых дескрипторов?
файловые дескрипторы, сокеты, мьютексы в конце концов. RAII дает куда больше чем простое освобождение памяти.
да обычный менеджмент ресурсов. Мы еще в 2000х писали auto critical sections, ничего тут особенного нет, раз открыл - закрой; а если еще и будешь делать это через спец-обьект на стеке, так и вообще молодец
вы сильно недооцениваете моё понимание плюсов и сильно переоцениваете моё понимание джавы/шарпа.
ну, вот у меня был такой опыт. Очень освежающий!! Когда через пару дней активной разработки мы вдруг сообразили - мама дорогая, это ж не дотнет, надо ж самим обьекты прибирать! И началось..
Единственный вариант когда managed heap теоретически может быть быстрее - если специфика нагрузки хорошо ложится на конкретный аллокатор.
? Managed heap - это непрерывный кусок памяти, уже заранее выделенный вирт. машиной, какие там спец аллокаторы. Для небольших обьектов никакой там магии нет
Подробнее можете ознакомиться в этом докладе
в докладе говорится, что гц добавляет оверхед - с этим-то я вообще не спорил; я говорил, что а) на современном железе на него чаще всего пофиг, б) помещение обьекта в управляемую память - быстрее
ну и докладчик не очень знает про современные стратегии сборки мусора:)
Парадигма ООП основана на инкапсуляции
нууууу... Об этом до сих пор спорят, на чем же она основана. А один из основателей так и вообще сказал, что он имел в виду скорее event driven, а не вот ЭТО
действительно, какие проблемы выжрать 300 гигов памяти на серваке, а потом потратить десяток секунд на её очистку, пока все остальные приложения ждут...
ну так если вы настроили вирт. машину так, что ей можно 300 гиг памяти, кто ж виноват
Кроме того, железо нынче дешевое, а программисты дорогие, так что, опять же, это не такая уж проблема.
Иными словами, пофиг, кто сколько сожрал памяти, если ее хватает
собственно в этом и проблема. Вы разве никогда не сталкивались с проблемами типа заканчивающихся файловых дескрипторов
нет, конечно, потому что я гарантированно закрываю открытые файлы
IDisposable - это просто интерфейс, позволяющий имплементировавшему его классу быть использованным в конструкции using. Как имплементацию напишете, так и будет - ровно как и в случае с деструктором вашего файлового обьекта
да обычный менеджмент ресурсов. Мы еще в 2000х писали auto critical sections, ничего тут особенного нет, раз открыл - закрой; а если еще и будешь делать это через спец-обьект на стеке, так и вообще молодец
это и называется "протекающие абстракции"
? Managed heap - это непрерывный кусок памяти, уже заранее выделенный вирт. машиной, какие там спец аллокаторы. Для небольших обьектов никакой там магии нет
это в теории там просто непрерывный последовательно заполняющийся кусок памяти. На практике это конечно же не работает (фрагментация уводит потребление памяти в космос) и приходится реализовывать полноценные аллокаторы, распределяющие память внутри этой managed heap.
б) помещение обьекта в управляемую память - быстрее
по сути это получается полноценная деаллокация. Грубо говоря, любой аллокатор, который используется внутри managed heap, можно просто подключить к плюсам и, если это выгодно, получить профит, безо всяких затрат на GC. Просто обычно это не делают потому, что это не выгодно.
ну и докладчик не очень знает про современные стратегии сборки мусора:)
а это не важно. Сколько ни оптимизируй/усложняй стратегию сборки мусора, отрицательной или даже нулевой её стоимость не станет.
нууууу... Об этом до сих пор спорят, на чем же она основана. А один из основателей так и вообще сказал, что он имел в виду скорее event driven, а не вот ЭТО
ООП по версии Алана Кая конечно больше похож на то, что нынче называют "акторами", но инкапсуляция в его парадигме всё так же фундаментально необходима
на современном железе на него чаще всего пофиг ... железо нынче дешевое, а программисты дорогие ... пофиг, кто сколько сожрал памяти, если ее хватает
Во-первых, вы предполагаете что разработка на языках с GC быстрее чем на тех же плюсах. Начиная с с++11 это как минимум не очевидно и не безусловно. Во-вторых, не всегда можно пренебречь стоимостью железа и не всегда есть его стократный запас. При сравнимой стоимости разработки ценник в $1m за облачную инфраструктуру покажется куда привлекательнее $5m.
это в теории там просто непрерывный последовательно заполняющийся кусок памяти. На практике это конечно же не работает (фрагментация уводит потребление памяти в космос) и приходится реализовывать полноценные аллокаторы, распределяющие память внутри этой managed heap.
да какая теория? При каждом коллекте gc дефрагментирует managed heap. Это гарантированно всегда непрерывный кусок памяти
Вот чесслово, почитайте доку уже
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals
а это не важно. Сколько ни оптимизируй/усложняй стратегию сборки мусора, отрицательной или даже нулевой её стоимость не станет.
не станет, но дьявол кроется в деталях
Во-первых, вы предполагаете что разработка на языках с GC быстрее чем на тех же плюсах. Начиная с с++11 это как минимум не очевидно и не безусловно
вполне очевидно; c++ заметно более сложный и дорогой в обслуживании язык. Начиная с разницы между обьектами, ссылками и указателями и заканчивая упоминавшимися вирт. деструкторами. Да и прочее, те же ексепшены взять
При сравнимой стоимости разработки ценник в $1m за облачную инфраструктуру покажется куда привлекательнее $5m.
вряд ли будет такая разница, цифры-то с потолка. При этом, найти толкового дева на плюсах сложнее и затратнее, чем, скажем, на дотнете или жаве - будь это иначе, не было б такой разницы в количестве тех и других
ООП по версии Алана Кая конечно больше похож на то, что нынче называют "акторами", но инкапсуляция в его парадигме всё так же фундаментально необходима
да все равно unique_ptr под это дело не очень попадает
инкапсуляция - сокрытие данных, которые определяют состояние обьекта, а указатель внутри up него не определяет, это просто обертка
да какая теория? При каждом коллекте gc дефрагментирует managed heap. Это гарантированно всегда непрерывный кусок памяти
ну по сути накладные расходы на аллокацию заменены куда большими во время сборки мусора, которая еще и блочит все треды.
не станет, но дьявол кроется в деталях
какой блин "дьявол" то? У вас есть дополнительные расходы на механизм, который своим существованием не может сэкономить ресурсы даже теоретически.
вполне очевидно; c++ заметно более сложный и дорогой в обслуживании язык. Начиная с разницы между обьектами, ссылками и указателями и заканчивая упоминавшимися вирт. деструкторами. Да и прочее, те же ексепшены взять
то, что язык сложнее, не значит, что писать на нём тоже сложнее. Взять например питон - на дистанции в 1-10 kloc писать на нём проще чем на чем угодно другом; писать на питоне проект на 100+ kloc - самоубийство.
вряд ли будет такая разница, цифры-то с потолка
ну оверхед джавы/шарпа относительно плюсов по CPU порядка 2-4 раз плюс стоимость stop the world, по памяти больше. Моя оценка не то чтобы консервативна, но в реальных пределах. По порядкам цифры тоже вполне реальные.
При этом, найти толкового дева на плюсах сложнее и затратнее, чем, скажем, на дотнете или жаве
вы посмотрите средние ЗП плюсовиков и джавистов, удивитесь
да все равно unique_ptr под это дело не очень попадает
инкапсуляция - сокрытие данных, которые определяют состояние обьекта
его задача - быть тонкой владеющей оберткой для объекта, размещенного в куче. unique_ptr инкапсулирует сырой указатель T* _ptr, не давая его менять кроме как через методы unique_ptr. По мне так тут всё совершенно логично и корректно. Нелогично было бы если бы приходилось для каждого объекта unique_ptr<T> писать финализатор, который вызовет ~T(), а память освобождалась хитрым и тормозным неявным способом, останавливающим все потоки исполнения
ну оверхед джавы/шарпа относительно плюсов по CPU порядка 2-4 раз
Я недавно делал замеры скорости работы алгоритмов обработки изображений на C# и C++: C++ оказывался в среднем всего на 20% быстрее, чем C#. Разница в разы была, наверное, лет 10 назад.
Недавно столкнулся стем что на Java LU-разложение быстрее чем на С++, на несколько процентов. Исходные коды идентичны.
Правда использовался GraalVM vs Visual C++.
ну по сути накладные расходы на аллокацию заменены куда большими во время сборки мусора, которая еще и блочит все треды.
ну так в реалиях-то как раз эта сборка происходит редко, да и сбор в первой генерации быстрый. При этом не будет out of memory из-за дефрагментации памяти
какой блин "дьявол" то? У вас есть дополнительные расходы на механизм, который своим существованием не может сэкономить ресурсы даже теоретически.
теоретически - да
А на практике - очень даже. Потому что сбор мусора происходит редко
то, что язык сложнее, не значит, что писать на нём тоже сложнее.
именно это оно и значит:))
Взять например питон - на дистанции в 1-10 kloc писать на нём проще чем на чем угодно другом; писать на питоне проект на 100+ kloc - самоубийство
ну так давайте не питон возьмем, а сишарп, или там, свят-свят, жаву
вы посмотрите средние ЗП плюсовиков и джавистов, удивитесь
да зачем мне смотреть, мы весь прошлый год искали
сишарп/жава - без проблем, а сишников тока в экс-ссср и нашли, дядьку уже за 40
Нелогично было бы если бы приходилось для каждого объекта unique_ptr писать финализатор, который вызовет ~T(),
ну в с++ наверное
а в сишарпе это просто деструктор
а память освобождалась хитрым и тормозным неявным способом, останавливающим все потоки исполнения
финализация идет в отдельном потоке, о чем вы?
И еще раз замечу - вы чересчур переоцениваете необходимость своевременной чистки памяти. В частных случаях - да, в целом нет
теоретически - да
А на практике - очень даже. Потому что сбор мусора происходит редко
чем реже запускается сбор мусора, тем больше ресурсов надо почистить, тем больше времени уходит на чистку. По сути это просто некоторые N% времени потраченные на GC. И этот N >> 0
именно это оно и значит:))
попробуйте написать что-то сколь угодно сложное на таком примитивно-тривиальном ЯП как баш и вы поймете о чем я
ну так давайте не питон возьмем, а сишарп, или там, свят-свят, жаву
ну так они на порядок сложнее питона, что в очередной раз подтверждает мой аргумент
да зачем мне смотреть, мы весь прошлый год искали
сишарп/жава - без проблем, а сишников тока в экс-ссср и нашли, дядьку уже за 40
может быть потому что вы искали "сишников" а не "плюсовиков"? Или потому что вы искали джуна, а спрашивали как с сеньора? В общем, сомнительный результат сомнительного эксперимента. В то время как рынок куда более объективен.
а в сишарпе это просто деструктор
который вызывается когда конкретно?
финализация идет в отдельном потоке, о чем вы?
синхронизированным с остальными, что ведет к их замедлению. Плюс, количество "отдельных потоков" и ресурсов CPU на их исполнение в общем случае не бесконечно.
И еще раз замечу - вы чересчур переоцениваете необходимость своевременной чистки памяти. В частных случаях - да, в целом нет
вот беру я рабочий ноут с CLion, вижу потребление RAM в 2.5 Гб, тыкаю "очистить", вижу 850Мб. Система репортит потребление CLion'а в 4.5 Гб + 1.3 Гб clang code model. Естественно настроен ShenandoahGC + ExperimentalVMOptions + GuaranteedGCInterval, т.к. без этого потребление памяти CLion'ом достигало 13 гигов а при инициации чистки мусора можно было смело идти пить чай. И ладно бы это было раз в день, а не каждые 1-2 часа. Вся эта память могла бы использоваться на что-то полезное, например на браузер. Вот это частный случай, или всё-таки общий?
RAII не даёт ничего, кроме, по факту, «выполни вот этот код при выходе из скоупа»
Во-первых, этого более чем достаточно противопоставляя "когда-нибудь GC подчистит память, остальное будь добр руками". Во-вторых, не обязательно "при выходе из скоупа". В третьих, вы опять придумываете гипотетические проблемы.
Такие гарантии (вместе с предсказуемым потреблением ресурсов) дают только типы.
давайте для начала вспомним что в чисто функциональном ЯП степень свободы изначально намного ниже и вводить/выдерживать контракты сильно проще. По факту если в плюсах писать в чисто функциональном стиле, пользуясь только иммутабельными объектами, вы тоже можете получить весьма надежный код.
А потом вы захотите управлять не только файлами на диске, но и чем-нибудь mmap'ленным, например, и понятие дескриптора файла надо будет как-то выносить отдельно.
не вижу проблемы. Можно расширить функционал имеющейся абстракции, можно ввести новую, можно ввести новую и потом обернуть вместе с имеющейся. В общем, заниматься разработкой. Не понимаю где что и чему тут мешает.
Мы обсуждаем производительность или корректность?
мне нравится как вы даже переспросили о чем спор, причем всего лишь во втором комментарии после того, как к нему присоединились. Сильно лучше некоторых.
Обсуждается производительность и универсальность соответствующих инструментов. Моя позиция - GC не может быть в общем случае быстрее и ограничивается лишь одним видом ресурсов - памятью, оставляя остальные на ручное удаление программистом.
Ну а когда RAII ещё срабатывает?
когда мы удаляем объект любым способом. Например, я же не выхожу из скоупа когда делаю optional<T>::operator=(nullopt)
, но эта операция удаляет вложенный объект при его наличии, вызывая его деструктор.
C++ приучил :(
выдумывать теоретически возможные ошибки, чтобы потом их у себя искать - весьма неэффективный подход к написанию и отладке кода.
Не понял, почему это?
как минимум лайфтаймы объектов вычисляются околотривиально, если ограничиться одними лишь чистыми функциями, принимающими иммутабельные аргументы по значению.
Плюс, ФП ≠ иммутабельность. В этих наших хаскелях вполне есть локальная мутабельность ...
мутабельность в ФП это бекдор, противоречащий парадигме. Собственно, поэтому и имеет смысл её локально ограничивать.
Ничему не мешает, просто показывает, что SRP таки нарушается.
можете показать где конкретно нарушается принцип единственной ответственности? Вот хоть убей не вижу.
Естественно, потому что любая последовательность событий освобождения ресурсов в GC может быть повторена вручную
а еще любая последовательность событий поверх аллоцировал/освободил является избыточной с точки зрения управления памятью. С++ позволяет мне этим ограничиться.
Однако, вы оставляете тут за скобками вопрос потребных человеческих ресурсов. Не надо так.
я искренне считаю что человеческий ресурс сравним, потому что с с++-специфичными ошибками я сталкиваюсь куда реже, чем с чисто логическими. Которые очевидно допустил бы и в других языках.
Часто встречаете такой код для
T ~ std::unique_ptr<T'>
илиmyDbConnection
?
сформулируйте вопрос иначе
Но на плюсах писать код с хоть какой-то степенью осознанности и уверенности нельзя.
define "какой-то степенью" потому что большая часть спектра возможных интерпретаций противоречит моему опыту. Вот буквально сегодня столкнулся с "invalid memory address or nil pointer dereference" в go приложении. Значит ли это что писать на go с "какой-то степенью осознанности и уверенности" тоже нельзя? Но ведь такие ошибки бывают и в c#/java, что про них? "А если нету разницы, зачем платить больше?" (с).
Ну а вот в ФП с точки зрения перформанса объекты принимаются по ссылке, а не по значению (с точки зрения семантики это неразличимо в ФП, понятное дело)
не важно как передается объект когда существует контракт что функция либо эксклюзивно им владеет, либо не может менять.
Там, где объект начинает отвечать и за абстракцию над fd, и/или за конкретный специальный случай обычных файлов на диске. Это разные вещи.
я волен инкапсулировать объект любым удобным мне образом. Захочу - сделаю тонкую обертку над дескриптором. Захочу - это будет представление "файла на диске". Или "файла на диске и/или в памяти". Или "файла лежащего по абстрактному URI". SRE не нарушится ни в одном из случаев, это всего лишь разные уровни абстракции.
Плюс, attention span у людей ограничен. Если мне нужно думать о плюсовых UB, то у меня остаётся меньше ресурсов на обдумывание логики.
Во-первых, это вопрос опыта разработки на конкретном языке, во-вторых, логика и реализация продумываются на разных этапах?
Часто засовываете в
std::optional
всякиеunique_ptr
ы или соединения с БД, и не для ленивой инициализации, а для того, чтобы одинunique_ptr
потом заменить другим?
не часто, но важно ведь не это. Важно то, что деструктор сработает при любом удалении объекта, а не только при неявном выходе из скоупа (чего в общем-то можно добиться и всякими defer'ами, пусть и многословнее).
Как часто они там встречаются?
как часто null deference встречается в современных плюсах? В тех, где сырые указатели в принципе используются сильно реже ссылок и умных.
У вас мир C#/Java ограничивается, что ли? Должно быть, это очень грустный мир.
это самые яркие примеры языков общего назначения с GC, которые в остальном похожи на плюсы. Не с полумертвым D же сравнивать.
Вы можете как-то сразу определиться со своими аргументами? То у вас все объекты передаются по значению, то это теперь неважно.
С точки зрения ABI передача по значению в с++ это создание нового объекта и передача его в функцию по ссылке. Если для объекта гарантируются свойства иммутабельности и неотличимости копии от оригинала, то копирование можно опустить и сразу передавать объект по ссылке. Собственно, в функциональных языках оба эти свойства могут гарантироваться, поэтому и не принципиально как там называется способ передачи
SRP вообще имеет смысл-то так? А то ведь для любого описания функциональности есть уровень абстракции, где это единый юнит.
Во-первых, и уровень абстракции, и функционал определяются во время проектирования, и тогда же можно однозначно ответить нарушается SRE или нет. Просто вы начали делать предположения про гипотетическую абстракцию, которую я еще не завершил проектировать, приведенную мной лишь для примера.
А во-вторых, забываете что принципы SOLID это просто свод указаний, а не жестких законов.
Ну вот моих 18 лет опыта, очевидно, недостаточно. Практика дырок в других приложениях показывает, что там тоже недостаточно.
опыт коммерческой разработки с 12 лет то?
Но только правила языка не гарантируют, что лайфтаймы объектов, зависящих друг от друга, будут соответствующим образом вложены, и в этом проблема.
проблема лайфтаймов ортогональна GC и решается без него (как в том же расте)
А зачем вы спорите с соломенными чучелами? Давайте лучше сравним количество висячих ссылок, выходов за границы и так далее.
хотите сказать в managed языках не бывает выхода за границы?
Зачем похожесть на плюсы? Императивно-нулловая философия накладывает свои ограничения.
так вся дискуссия про то, насколько полезен GC. Соответственно желательно сравнивать языки, которые максимально близки во всем, кроме наличия/отсутствия GC.
А если я его после этого сохранил как поле объекта?
вариантов уйма же, самый простой - копирование.
Я не то что забываю, я изначально во всех этих солидах не шарю, там сложно чё-т слишком.
вы постоянно находите простейшие вещи невероятно сложными... И наоборот
Ну наконец-то вы сами начали подходить к тому, о чём я говорил изначально — о типах, то бишь.
в дискуссии о полезности GC?
Хочу сказать, что в managed-языках без unsafe выход за границы отлавливается и не является UB.
Это всё еще ошибка, она всё еще допущена, код всё еще работает некорректно. Более того, для неожиданных исключений обычно отсутствует нормальная обработка. В итоге приложение приложение уходит в некоторое некорректное состояние, в котором может застрять до рестарта (недавно ловил такое). По мне так лучше краш + рестарт.
Так или иначе, от ошибки мы не избавились, просто получили возможность запихать её под ковер.
Нет. Имеет смысл рассматривать языки, которые без GC реализуются тяжко (функциональщину там всякую).
предлагаю вам найти более компетентного собеседника для споров про функциональщину. Я имею поверхностное представление о том, каким должен быть чистый функциональный язык, но не знаком со всем набором распространенных костылей и хаков, которые делают реальные функциональные языки применимыми.
Ну ещё раз, скопируйте мне соединение с БД.
shared_ptr<DBConnection> conn(otherConn);
?
В дискуссии о методах управления ресурсами.
GC не является "методом управления ресурсами", а только лишь памятью.
Нет, код не работает некорректно.
"Код не работает корректно", whatever. Вы пытаетесь продать исключение при выходе за границу так, словно оно предотвратило ошибку. Однако надежность системы не выросла ни на йоту.
Код вообще не работает, потому что его выполнение прервалось экзепшоном.
"Прервалось экзепшном" != "не работает". Максимально вероятно код будет продолжать работать каким-то не запланированным способом. Так же как и в плюсах. Но у вас почему-то в одном языке в выходе за границу массива будет виноват программист, а в другом языке - сам ЯП. Где в вашем сознании пролегает та самая эфемерная граница, их разделяющая? Возможно это банальная предвзятость?
Очень по значению, ага.
От того, что вы указатель завернули в класс и класс передаёте по значению, вся программа в «только по значению» не превратилась.
Я не понимаю чего вы от меня добиваетесь. Я ж говорю - передавать объекты в функции в абстрактном чисто функциональном языке можно практически как угодно. Вроде логично, что любой иммутабельный объект можно сделать refcounted, реализуя тем самым его копируемость.
Если у меня есть условный торговый бот, который рассчитывает, торговать или не торговать, то я бы предпочёл, чтобы при ошибке доступа за границы он упал, чем если бы он просто модифицировал соседний массив флоатов с весами для какой-нибудь регрессионной модели, которая после этого начнёт выдавать фигню.
А владелец этого торгового робота предпочел бы, чтобы этот робот делал свои расчеты быстрее роботов конкурентов. А динамические проверки в ран-тайме на скорость неизбежно скажутся и не лучшим образом. Посему пойдет ваш корректный, но небыстрый, робот туда же, куда и быстрый, но некорректный.
К счастью, кроме динамических проверок есть возможность что-то про код доказывать статически (про что я тоже писал).
И тогда рассматривать пример с динамической проверкой границ и выбросом исключения становится бессмысленным.
Ну или замените торгового бота на программу, рассчитывающую дозу рентгеновского аппарата.
Насколько я помню ту историю, там одной из проблем стало то, что разработчики не подумали о том, что пользователь может вводить данные на пульте
Как определить C и C++-программистов по коду, который они пишут