Показаны сообщения с ярлыком Павбывавбы. Показать все сообщения
Показаны сообщения с ярлыком Павбывавбы. Показать все сообщения

понедельник, 15 июня 2026 г.

[prog.c++.imho] Не согласен с постулатами пропозала P3097 (контракты для виртуальных методов)

Комитет по стандартизации C++ продолжает творить дичь. Сперва в C++26 были включены кастрированные контракты (нет ключевого слова old в постусловиях, нет контрактов для виртуальных методов, нет инвариантов для экземпляров классов и циклов). Для людей, знакомых с Eiffel, контракты из C++26 выглядят как "мы не осилили тему полностью, поэтому впихнули в стандарт какой-то эрзац с надеждой, что со временем допилим". Не хочу обсуждать зачем нужен эрзац вместо нормального продукта. Просто перейду к следующей дичи.

Далее в C++29 включили предложение P3097, которое описывает контракты для виртуальных методов классов. И авторы этого предложения, как по мне, покусились на святое: на сформулированное много-много лет назад для Design By Contract в Eiffel-е требование о том, что производный класс может только ослабить предусловния и ужесточить постусловия, но не наоборот.

И вот авторы пропозала почему-то пришли к выводу, что C++ настолько особенный, что в нем можно это требование послать в пешее эротическое.

На протяжении нескольких страниц пропозала эти люди пытаются приводить "аргументацию" своей точки зрения. Меня эта аргументация не убеждает от слова совсем. Скорее наводит на мысль о том, что люди толком не понимают тему, о которой пытаются рассуждать и, скорее всего, не имеют опыта разработки на языках с поддержкой Design By Contract (в первую очередь на Eiffel-е, на который в данной теме и следует равняться).

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


В разделе "3.2 Adoptability in legacy code" есть интересный заход:

среда, 15 апреля 2026 г.

[prog.sadness] Вскрик души в процессе копания в чужом коде

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

PascalCase -- отстой.

PascalCase в совокупности с длинными строками и экономией на пробелах и пустых строках -- отстой вдвойне.

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

Удачно выбранные имена классов рулят. Неинформативные имена или имена, отличающиеся всего одной буквой (например, resource_handler и resources_handler) доставляют (в худшем смысле этого слова) неимоверно.

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

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

Но главное впечатление, еще более субъективное, личное и неутешительное для меня самого: очень сложно работать с кодом, написанным по принципу "сейчас кое как слепим, а потом переделаем по нормальному". Пытаясь разобраться с результатом главная мысль в голове -- "Господь, жги, тут уж ничем не помочь". А жечь то как раз и нельзя 😡

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

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

среда, 24 декабря 2025 г.

[c++.flame] Желтушные заголовки: Microsoft собирается переписать весь Сишный и C++ный код на Rust!

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

Настоящая шок-сенсация в статье на Хабре:

Microsoft планирует модернизировать свои крупнейшие кодовые базы и к концу десятилетия полностью исключить весь код на C/C++, заменив его на Rust.

Со ссылкой на источник на английском языке, который говорит о том же:

Microsoft is taking an impressive step in modernizing its biggest codebases and will eliminate all C/C++ code by the end of the decade, replacing it with Rust.

Для старых упоротых C++ников, вроде меня, новость из категории "А-А-А, мама, мы все умрем! Вот прям завтра помрем вааапще все!!!"

Признаться, я сам купился на заголовки поленившись дойти до первоисточника. А стоило бы...

На самом деле некий инженер из Microsoft, работающий над экспериментальным проектом, запостил у себя в LinkedIn ссылку на вакансию в своей команде. Мол, ищем разработчика под амбициозный проект. И личной целью этого самого инженера является полная замена кода на Си и C++. Личной целью конкретного руководителя какого-то экспериментального проекта внутри Microsoft.

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

Бзик конкретного человека (я бы сказал, безумная вера в утопическое будущее) разогнали до планетарного масштаба.

Тьфу, срамота.

понедельник, 24 ноября 2025 г.

[prog.c++] Не нужно делать собственные специализации std::hash<std::pair<T,U>>

Некоторое время назад я сам наткнулся на грабли при использовании библиотеки Folly. Мне потребовался std::hash для std::pair<int, some_my_type>, при этом std::hash для some_my_type у меня уже был. Поэтому я сделал специализацию std::hash для std::pair<int, some_my_type>, в которой использовал std::hash<some_my_type>. Собрал код, стал проверять и выяснил, что на некоторых тестовых сценариях происходит что-то странное. Начал разбираться и внезапно обнаружил, что мой std::hash для std::pair<int, some_my_type> не вызывается вообще.

Дело оказалось в том, что разработчики Folly подложили свинью своим пользователям и определили специализацию std::hash<std::pair<T, U>>. Чего делать по стандарту нельзя (за цитату из стандарта большое спасибо Константину Шарону):

• [namespace.std] (C++23 draft, §16.4.2.2/1):
The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std, unless otherwise specified.
• The only allowed exception is that you may specialize certain templates in std for user-defined types.

[unord.hash] (C++23 draft, §24.5.4.2/1): For all object types Key for which there exists a specialization std::hash, the program may define additional specializations of std::hash for user-defined types.

Тут особый упор нужно сделать на "for user-defined types", тогда как std::pair<T, U> -- это не user-defined type. Вот std::pair<int, some_my_type> -- это уже user-defined type и для него специализацию std::hash делать можно.

Об этом несколько месяцев назад я писал в LinkedIn. Но недавно на Reddit-е обнаружил ссылку на статью Simplify hashing in C++ в которой, ни много, ни мало, предлагается делать тоже самое, т.е. определять специализации std::hash для std::pair<T, U>.

Поскольку такое заблуждение существует, то приходится заострять на этом внимание. Поэтому, пожалуйста, не делайте собственные специализации std::hash<std::pair<T, U>>.

А если вы сомневаетесь в правильности того, о чем я говорю, то представьте себе ситуацию:

  • вы разрабатываете программу и определили какой-то собственный тип your_type;
  • вы захотели использовать your_type в качестве ключа в unordered_map и сделали для your_type специализацию std::hash;
  • затем вы захотели сэкономить себе время и силы и сделали специализацию std::hash для std::pair<T, U>. Теперь у вас автоматически поддерживается хеширование и для std::pair<int, your_type>, и для std::pair<your_type, int>, и прочих сочетаний your_type с другими типами, поддерживающими кэширование;
  • где-то в программе вы используете std::unordered_set<std::pair<int, your_type>>
  • все у вас работает нормально;
  • затем в один прекрасный момент вы подключаете в проект стороннюю библиотеку, в которую отдаете your_type, и которая использует std::pair<int, your_type> в качестве ключа в Folly::F14FastMap;
  • и тут оказывается, что у вас в проекте есть два определения для std::hash<std::pair<int, your_type>>

Поздравляю, у вас в коде Undefined Behaviour из-за нарушения one definition rule.

Реально хотите собственными руками подсаживать UB в код?


Разработчиков Folly понять можно: они делали Folly для своих собственных нужд и в их программах, разработанных под задачи Facebook-а, вряд ли могут встретиться другие специализации для std::hash. А то, что Folly применяют и за пределами Facebook-а, в том числе смешивая в одном проекте Folly с кучей других библиотек -- это не проблемы разработчиков Folly.

четверг, 21 августа 2025 г.

[prog.c++] Я бы розгами прививал C++никам привычку использовать using для псевдонимов типов

На первом курсе ВУЗа нас учили языку Pascal. Пожалуй, лучшее, что удалось взять из Pascal -- это привычку давать имена типам. Полагаю, возникла она потому, что преподаватели хорошенько нам разъясняли почему это правильно, полезно и удобно.

Эта привычка помогала и после перехода на Си, а затем и на C++.

Почему-то мне казалось, что в те старые времена и в Си, и в C++ была культура использования typedef-ов для создания псевдонимов типов. Вероятно из-за того, что платформ было много, платформы были разные. А псевдонимы, сделанные через typedef, помогали справляться с их различием. Достаточно вспомнить приключения при переходе от 16 бит к 32 битам и обратно.

Но сейчас складывается впечатление, что многие C++ разработчики про typedef и using даже и не знают. Вот реально приятно удивляешься, когда открываешь чужой проект -- а там систематическое применение using-ов (или typedef-ов). Обычно же все гораздо печальнее и приходится иметь дело с чем-то вроде:

void do_something(
  std::vector<std::pair<int, std::shared_ptr<my_obj>>> & first,
  std::map<std::string, std::tuple<int, int, std::string>> & second);

А еще хуже, на мой взгляд, когда один и тот же фундаментальный тип (вроде int-а или unsigned long-а) используется для совершенно разных целей:

struct some_item {
  int _x;
  int _y;
  int _width;
  int _height;
  int _usage_percentage;
  int _reusing_mode;
  int _logging_level;
  ...
};

Когда со временем меняешь int на что-нибудь другое, то обязательно выясняется что где-то как-то параметры перепутали -- выдали _width за _height или _usage_percentage вместо _reusing_mode.

Конечно, от многих подобных проблем в C++ можно было бы избавиться, если бы в C++ из коробки была поддержка strong typedef. Но даже в ее отсутствии обычные псевдонимы типов, сделанные через using, сильно повышают читабельность кода и упрощают его сопровождение (или портирование на другую платформу).

Вроде бы уже не раз говорил в разных местах о достоинствах using-ов:

  • сокращение присанины, приведение кода к более лаконичному и, как правило, более понятному виду;
  • сокрытие деталей реализации, что помогает затем менять эти самые детали всего лишь в одном месте, но сказываться замена будет автоматически повсюду.

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

Но если мне это очевидно, то почему это не очевидно другим C++разработчикам?

Возможно потому, что им не привили правильные привычки во время обучения.

Отсюда проистекает мысль, что если учить людей C++у, то нужно вкладывать им в голову не только идею RAII, как одну из самых основополагающих, но и идею повсеместного применения using-а. А всякий, кто не внемлет мудрости старших, должен быть порот розгами, дабы полезная наука усвоена была, пусть и через другое место.

PS. Очень надеюсь, что рано или поздно, но C++ обзаведется штатным, доступным прямо в стандартной библиотеке, механизмом strong typedef. И если в вашем коде using используется должным образом, то переход к применение strong typedef-а окажется более простым и менее болезненным.

суббота, 12 июля 2025 г.

[prog.c++] Примеры нововведений в С++, которые "осчастливили" меня (на самом деле нет)

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

Хочу перечислить здесь несколько случаев когда в стандарт C++ вносили нечто, что вызывает у меня неоднозначные чувства, а иногда и прямо-таки негативные. ИМХО, в каждом из этих случаев можно было либо вообще отказаться от нововведений, либо же сделать их иначе.

Описанное ниже -- это только наиболее яркие случаи, которые сразу вспоминаются. Полагаю, что таких ситуаций гораздо больше. Каких-то, вроде uniform initialization syntax, даже касаться не хочется, настолько удручающим получился результат.

Ладно, хватит предисловий, двайте начнем.


std::launder. Очень специфическая штука, которую мало кто понимает. И, что раздражает больше всего, ведь до введения std::launder в стандарт код у людей работал. И сейчас у многих работает. Но может внезапно перестать. При этом если опросить 1000 случайных C++ разработчиков, то, боюсь, малый процент из них вообще сможет внятно рассказать для чего std::launder в принципе нужен. И еще меньший процент сможет объяснить где же std::launder должен применяться, а где нет.

При этом std::launder имеет отношение к такой штуке как lifetime, но когда вводили std::launder почему-то не включили в стандарт никаких других инструментов для объявления начала lifetime в остальных случаях, не покрываемых std::launder. Например, абсолютно необходимая во многих ситуациях std::start_lifetime_as была добавлена только в C++23. При том, что многие проекты все еще на С++17 и непонятно когда на C++23 смогут перейти.

Т.е. в C++17 разработчиков поставили перед фактом о том, что теперь компилятор может начать эксплуатировать UB, связанные с lifetime-ами. И лишь в C++23 программистам дали нормальный набор инструментов.

При этом в C++20 сделали некую полумеру -- объявили набор магических функций и ситуаций, когда lifetime неявно начинается. Типа std::malloc начинает lifetime, а какая-то произвольная пользовательская функция, скажем, my::shared_memory_arena::malloc -- нет, потому что стандарт про нее не знает, и стандарт не дает никаких возможностей указать, что пользовательская функция ведет себя как malloc.

Если смотреть на всю эту эпопею с std::launder в C++17, implicit lifetimes в C++20 и std::start_lifetime_as в C++23, то складывается ощущение, что люди из комитета тупо затыкали дыры по мере их обнаружения. Вместо того, чтобы провести глубокий анализ и выкатить полноценное решение, а не набор отдельных "заплаток", да еще и растянутых на период в шесть лет.

Хотя, как по мне, так все эти заморочки с lifetime-ами нужно вообще выбросить. И если эта штука так важна разработчикам компиляторов, то пусть с ней разбираются разработчики компиляторов, а не обычные C++ программисты, которым и так граблей насыпают с каждым стандартом.


Модули.

Тут мне вообще сложно говорить цензурно.

Само появление модулей в стандарте означает что все, кто на свою беду сделал собственные системы сборки С++ных проектов, теперь вынуждены будут либо допиливать их до поддержки модулей, либо же менять на что-то (а что-то -- это скорее всего CMake).

Кроме того, есть у меня ощущение, что модули не дадут заметного положительного эффекта в случаях, когда в коде используется много шаблонов. Ибо когда сейчас имеешь дело с таким кодом, то даже невооруженным взглядом видно, что основное время тратится не на парсинг C++ного кода, а на инстанцирование шаблонов и их оптимизацию. Т.е. если у меня сейчас .cpp-файл транслируется 20 секунд, включая все include и пр. расходы от которых типа должны освободить модули, то с модулями этот файл будет транслироваться 19 или, если поведет, 18 секунд. Отличное достижение, прям таки победа.

Может быть какой-то выигрыш от модулей был заметен в 2019-ом на тогдашних CPU и HDD-дисках. Сейчас, когда даже в бюджетных ноутбуках стоят CPU с десятком(!) ядер и SSD со скоростями больше нескольких гигабайт в секунду, проблем с длительной обработкой большого количества include нет от слова совсем.

Ну и главное: сейчас уже середина 2025-го года, а нормальной полноценной поддержки модулей в большой тройке компиляторов нет.

И спрашивается: а оно вообще стоило того? Угробить кучу ресурсов на внедрение в стандарт, компиляторы, IDE и пр. инструменты поддержку явно переусложненной системы модулей, которая вносит в C++ такой же раскол, как Python3 по отношению к Python2. И не получить вменяемого результата даже через пять лет после выхода С++20.

Это, блин, полный успех.

Попробовали бы в какой-то коммерческой разработке так долго "скармливать" заказчику появление подобной "супер-мега-киллер-фичи". А над обычными C++никами можно. Раз уж мы решили оставаться в этом языке, то нужно страдать и ждать обещанного счастья по 5 лет.


Неоднократно поминаемый мной deducing this. Не буду много распространяться, просто дам ссылку на свой же пост двухлетней давности: тыц.

Что меня раздражает в deducing this больше всего, так это не то, что в C++ добавили очередной всратый синтаксис для того, чтобы сделать тоже самое, но совсем другим способом, а то, что deducing this вообще не дружит с DLL/SO библиотеками.

Вот хотите вы экспортировать из DLL/SO свой C++ный класс. И в этом классе есть дублирующиеся методы, количество которых хотелось бы подсократить посредством deducing this. Но вы этого не сможете, потому что deducing this работает по принципу C++ных шаблонов.

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

Идиотия чистой воды. Но, видимо, кто с Python-измом или Rust-оизмом головного мозга очень хотел превратить C++ в подобие Python/Rust с явной передачей self-а в методы класса. И у этого "кого-то", что характерно, получилось 🙁


Включенная в C++26 рефлексия времени компиляции.

Тут я нахожусь в полной растерянности от выбранного синтаксиса.

Во-первых, каким боком ^^ к рефлексии? Ведь в C++ есть оператор ^, как этот оператор соотносится с рефлексией? Никак? Здесь нет логики, нужно просто запомнить, что ^ -- это про операции над битами (если он не перегружен, конечно), а ^^ -- это рефлексия? Ну так чем ^^ лучше любого другого единичного символа? Почему ^^T, а не @T или %T или даже `T?

Во-вторых, почему "операторные скобки" для рефлексии используют двоеточие? Оно же тупо плохо заметно в тексте в комбинациях [: и :]. И, кроме того, оно вообще никак не соотносится с ^^.

Т.е. нужно просто зазубрить, что получить метаинформацию -- это ^^, а использовать метаинформацию -- это через квадратные скобки с двоеточием.

Логично и последовательно, да.


В общем, что хочу сказать в завершение...

Я сам очень не люблю, когда на комитет по стандартизации C++ наезжают, особенно когда людей оттуда обвиняют в дебилизме, преследовании собственных шкурных интересов и пр. Многие комитетчики работают на чистом энтузиазме и благодаря их труду (который может и не оплачиваться вовсе) C++ все еще развивается. При этом сам я никакого участия в этом процессе не принимаю участия и, вроде как, не имею морального права высказывать негатив в адрес комитета...

Но мне начинает казаться, что комитет уже много лет живет какой-то собственной жизнью и отрывается от реальности. В результате чего в языке появляются возможности, на которые смотришь и все что можешь сказать, так это "спасибо, конечно, за новую фичу, но, блин, почему так всрато-то?"

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

Ну и да, комитету насрать на то, когда приятое им в стандарт появится в большинстве компиляторов в приемлемом качестве. Поэтому давайте штамповать фичи, которые доберутся до рядового разработчика через 5-6-7 лет. А то и не доберутся вовсе, как export template из С++98. Пофиг, пипл схавает.

Поэтому еще раз повторю то, что уже писал в разных соцсетях:

Ранее C++ делали люди, гораздо более умные, чем я, но делали они это для таких как я. Сейчас C++ делают люди, гораздо более умные, чем я, но делают они это уже только для себя. И проблема в том, что они недостаточно умны для этого.

четверг, 29 мая 2025 г.

[prog.c++] Не нужно использовать расширение .h для имен заголовочных файлов в C++

В именовании файлов должна быть и логика, и практичность.

Например, использование расширения .h для заголовочных файлов С++, как по мне, и не логично, и не практично.

Нелогично с точки зрения того, .h используется в чистом Си. И казалось бы, если у нас есть некий foo.h, то его можно было бы подключить и в .c-файле, ведь софт на Си все еще пишут. И иногда в Си-шный софт подключают фрагменты, написанные на C++, выставляющие наружу чисто Си-шный интерфейс. Но нет, оказывается, что в .h лежит не C, а C++.

Непрактично с точки зрения поиска в исходных текстах. Например, у меня есть каталог с исходниками, хочу найти те файлы, в которых встречается какая-то подстрока. Какую маску имен файлов задавать для grep? А если у меня в распоряжении не grep, а FAR Manager в Windows? Или если я привык пользоваться Midnight Commander и не хочу бодаться с дедовским grep?

Гораздо удобнее использовать расширения .hpp для заголовочных, и .cpp для файлов реализации. Ну или .hxx и .cxx.

Почему?

Во-первых, сразу видно, что мы имеем дело с C++ и не можем задействовать заголовочный файл в чисто Си-шном коде.

Во-вторых, маски поиска (хоть для grep, хоть для FAR, хоть для MC) задаются не просто, а очень просто: *.?pp (ну или *.[hc]pp для тех, кто любит точность).

А расширение .h в C++ных проектах нужно оставить для кода, который совместим с чистым Си.


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

Удобство расширения .ipp в том, что оно попадает под ту же самую маску поиска: *.?pp.


Да, я знаю, что модули C++ убирают проблему именования заголовочных файлов. Но в моей вселенной прекрасное будущее с модулями C++ еще не наступило. Так что пока маемо шо маемо.

А вот на счет адекватности людей, пропихнувших модули в C++ у меня есть сомнения. Но это уже совсем другая история.


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

суббота, 19 апреля 2025 г.

[prog.c++] Наткнулся на образчик кода из категории "Да не дай боже такое сопровождать!"

В очередной раз с трудом удерживаюсь, чтобы не ввязаться в публичное обсуждение на профильном ресурсе. В этот раз опять на RSDN ;)

Недавно там образовалась тема с самодельным аналогом std::format/fmt::format. Над происходящим в нёй я уже слегка поугорал в LinkedIn. Но т.к. автор сего велосипеда в излишне поучительном (на мой субъективный взгляд, конечно же) тоне выступает в другом треде, то решил краем глаза вглянуть на то, какой же код производит данный оратор. Ну по принципу talk is cheap, show me the code.

Заглянул в первый попавшийся файл и прифигел (если выражаться цензурным языком, что непросто). Там функция на 700+ строк. Причем вряд ли сгенерированная автоматически, больше похоже на написанную вручную. Еще и с goto, для полноты ощущений.

Как по мне, так подобное не что иное как говнокод. Говнокод как он есть. В чистом, дистиллированном виде.

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

А еще вспоминается очень точный в данном случае афоризм: код должен быть настолько тривиальным, чтобы было очевидно, что в нем нет ошибок, либо же он должен быть настолько нетривиальным, чтобы было неочевидно, если ли в нем ошибки. Вот как раз увиденный мной фрагмент про второе.


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


Простите, дальше будет совсем грубо.

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

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

Но при этом этот работающий код оказывается откровенным говном. Как в примере выше.

Но работающим же.

И когда такому коллеге пытаешься объяснить, что вообще-то так нельзя, у них есть убийственный аргумент: "Так оно же работает!"

Противопоставить такому аргументу лично я могу только "Ну OK, только я ни за что не буду это сопровождать".

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

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

понедельник, 31 марта 2025 г.

[prog.c++] Наследие темного прошлого C++, которое сейчас стало напрягать все больше и больше

Зафиксирую несколько моментов в C++, которые в последнее время напрягают, раздражают и, как правило, ведут к эпизодически проявляющимся ошибкам. Они были унаследованы из старых темных времен, являются наследием Си, но отравляют жизнь до сих пор. Хотя, наверное, не всем, а лишь тем, кто пытается следить за качеством кода 😡


Неявные приведения типов.

В каких-то случаях это удобно. Напишешь ты что-то вроде:

void dump_value(std::string_view value) {...}

а потом спокойно пихаешь туда строковый литерал:

dump_value("Hello, World!");

и все работает.

Хотя, чем старше становлюсь и с чем большим объемом чужого кода приходится иметь дело, тем больше склоняюсь к тому, что писать так:

dump_value("Hello, World!"sv);

или даже так:

std::string calculated_value = some_calculation();
dump_value(std::string_view{calculated_value});

тоже вполне себе OK.

Но вот что откровенно вымораживает, так это то, что язык допускает неявные преобразованиями между int-ами, long-ами, float-ами, double-ами и прочими числовыми типами. Иногда нужно включать высокие уровни предупреждений для того, чтобы компилятор хоть как-то пожаловался на выражения вида:

std::vector<item> items = collect_items();
int delta = items.size() > 3 ? 2 : 0;
std::size_t limit = (items.size() - delta) * 1.75;

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

Очень хочется, чтобы компилятор запретил все подобные неявные преобразования между числовыми типами. Чтобы ошибка выдавалась даже при попытке неявно преобразовать std::uint8_t в std::uint16_t. Не говоря уже про преобразования от std::size_t к short или от double к long.


Использование "безразмерных" типов short, int, long и пр.

Да, я помню, что Страуструп агитировал за int. И он же считал ошибкой, что std::size_t в STL сделали беззнаковым.

Но мы сейчас живем в мире 64-х битовых архитектур и в ситуации, когда на машине несколько десятков гигабайт памяти уже не редкость. Причем на таких машинах решаются задачи в которых вся эта память используется. И совсем уже не экзотика, когда у нас может быть вектор с числом элементов больше 2 миллиардов или байтовый блоб размером больше 4GiB. Но при этом 32-х битовые архитектуры так же все еще встречаются и один и тот же исходный код может быть скомпилирован как в 32-х, так и в 64-х битах. И запущен он может быть на разных машинах с разным объемом памяти и исходных данных совершенно разного размера.

Поэтому когда я сейчас вижу в программах что-то вроде:

int l = strlen(some_string);

или

int last_element_index = vec.size() - 1;

то у меня буквально руки опускаются. Хорошо, пусть в большинстве случаев это безобидный код. Но, блин, как такие ошибки ловить в ситуациях, когда данные у нас оказываются реально большими? И зачем, спрашивается, эти самые ошибки ловить?

Поэтому я вообще склонен запретить использовать в коде типы, размерность которых никак не зафиксирована (short, unsigned short, int, unsigned int, long, unsigned long и т.п.).

Грубо говоря, int-у место разве что в примерах в старых учебниках. Но в современном предназначенном для продакшена коде, ему не должно быть места. Следует использовать либо типы с зафиксированной размерностью (std::uint8_t, std::uint16_t, std::uint32_t и т.д.), либо типы с обещанной минимальной размерностью (std::uint_fast8_t, uint_least8_t, std::uint_fast16_t, std::uint_least16_t и т.д.).


И еще один момент, которого в C++ нет, но который, имхо, был бы полезен.

ЕМНИП, в Pascal можно было объявить перечисление из, скажем, трех элементов, а затем создать массив, для индексации которого может использоваться именно это перечисление. Что-то вроде:

type
  Dimensions = (Price, Speed, Riskiness);
  Corrections = array of [Dimensions] of Real;
var
  CurrentCorrections : Corrections;
begin
  CurrentCorrections[Price] := 1.0;
  CurrentCorrections[Speed] := 0.5;
  CurrentCorrections[Riskiness] := 1.25;
  ...

При этом вы точно знаете, что ваш объект CurrentCorrections тесно связан с Dimensions. И если со временем ваш Dimensions меняется, то это сказывается и на работе с CurrentCorrections.

Тогда как в C++ такой фичи нет. Мы можем сделать так:

enum class Dimensions { Price, Speed, Riskiness };
using Corrections = std::array<double, 3>; // А вот тут уже первая неприятность.
...
Corrections current_corrections;
current_corrections[Dimensions::Price] = 1.0;
current_corrections[Dimensions::Speed] = 0.5;
current_corrections[Dimensions::Riskiness] = 1.25;

Но надежность этого кода будет исключительно на совести и внимательности программиста.

Например, у нас нет возможности узнать сколько элементов в перечислении. Как и нет возможности узнать значения элементов этого перечисления (чтобы выяснить монотонно ли они возрастают с шагом в 1 или нет).

Поэтому со временем кто-то может модифицировать перечисление Dimensions, например, вот так:

enum class Dimensions { Price = -2, Speed = 0, Riskiness = 3, Effectiveness = 10 };

и весь старый код, включая ставшее неправильным определение типа Corrections, все равно скомпилируется. А потом хорошо, если быстро упадет.

Может быть рефлексия, которую обещают завести в C++26, поможет сделать что-то подобное. Но еще вопрос когда C++26 получится попробовать в реальном проекте, да еще и за чужой счет. И даже когда в проектах начнет повсеместно применяться C++26, то как много C++ников захочет велосипедить что-то подобное на этой самой рефлексии?

суббота, 29 марта 2025 г.

[prog.kill-them-all] И снова слова проклятия в адрес любителей длинных строк в исходном коде...

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

Пример, переполнивший сегодня чашу терпения на скриншоте. Это полный скрин моего основного Windows-ноутбука. Именно так все выглядит на экране в обычном режиме.

Что обидно, так это то, что иллюстрационный фрагмент содержит так же и пример нормального оформления (выделен зеленым). Может быть даже с излишними переносами строк. По крайне мере у себя бы я friend и STRONG_CONSTEXPR разместил бы на одной строке.

Ну и да, привет всем тем чудикам, которые утверждают, что код читают только в IDE. Да хрен вам!

Так что вредный совет тем, кто по какой-то причине обратил внимание на этот пост: если вам начхать на удобство тех, кто будет читать и сопровождать ваш код в дальнейшем, то смело пишите строки длиной по 100, 120, 150 символов. Да и 150 не предел, зачем останавливаться?

PS. Прошу понять и простить за резкость, но за минувшую неделю пришлось слишком много проблемных фрагментов кода копипастить из исходников в email-ы, мессенджеры и GoogleDoc-документы. Где куски с длинной строки в 120+ символов превращались в совсем уж нечитаемое ХЗ что. В отличие от.

PPS. Надо бы взять на вооружение именно этот способ записи выражений noexcept в декларации методов. Выглядит лучше чем то, что пишу я.

четверг, 28 ноября 2024 г.

[soft.dev.wtf] Мир сходит с ума... На примере маразма с "The Undefined Behaviour Question"

Просто зафиксирую эту историю здесь, т.к. мне она кажется показательной. Если кто не в курсе, то суть, как я ее понял, в том, что в комитет по стандартизации C++ было подано предложение под названием "P3403 The Undefined Behaviour Question". Кому-то из комитетчиков в названии увиделась аналогия с "еврейским вопросом" (Jewish Question) времен гитлеровской Германии. И автора предложения попросили поменять название. Он сперва раздумывал об этом, но потом решил оставить все как есть, т.к. придерживается мнения, что вопрос об undefined behaviour в языке программирования не имеет ничего общего с геноцидом по этническому признаку. После чего (как я понял) его предложение отказались пропускать через комитетскую бюрократию, а его самого подвергли обструкции.

Эта история всплыла на Reddit-е, в разделе /r/cpp, откуда была удалена, но осталась в другом разделе. А потом и сам автор предложения "The Undefined Behaviour Question" подробно изложил свою версию происходивших событий: First-hand Account of “The Undefined Behavior Question” Incident.

Ну что здесь остается сказать?

Сперва ветку master в git-е переименовали в main из-за того, что когда-то в США "master"-ом называли рабовладельцев, а сейчас git checkout master вызывает жуткие страдания у современных чернокожих программистов (ага, ага). Теперь вот это. Верной дорогой, товарищи. Прям как во времена СССР -- а нет ли в названии художественного произведения скрытой антисоветчины?

ИМХО, мир становится глобальным и чем больше это происходит, тем более диким выглядит то, как какая-то маленькая (в масштабах человечества) группа хер знает кого диктует всем остальным свои правила.

Это тем более дико на фоне того, что разработчикам из РБ и РФ обнуляют аккаунты на BitBucket-е, отказываются принимать от них pull request-ы, блокируют доступ к техническим ресурсам в Европе и США. Просто по географическому признаку: мол, если ты из РБ, то ты подлежишь обструкции. Просто потому что.

И если уж тут всплыла тема "еврейского вопроса", то нет ли здесь каких-то аналогий? Ну по типу навешивания "вины" просто по одному общему критерию -- раз из РБ, значит виноват в войне на Украине. Вроде бы где-то такое уже было? Что-то типа: ну, раз еврей, значит виноват в распятии Христа и пожирании невинных младенцев, и выпивании всей воды из крана.

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

PS. Если данный пост позволил кому-то рассмотреть во мне замшелого ватника, застрявшего в СССР, то поздравляю, ваши розовые очки в очередной раз треснули от столкновения с реальностью.

понедельник, 9 сентября 2024 г.

[prog.c++] Сюрприз с размером производного класса в разных компиляторах

С размером вот этой простой структуры на x64 при выравнивании по умолчанию все понятно:

struct base {
    void * m_ptr;
    short m_sh;
    char m_ch;
};

Вполне ожидаемо имеем 16 байт. Из которых реально используются 11, а остальное -- это выравнивание на границу 8 байт (т.к. указатель на 64-х битовых платформах занимает 8 байт).

А вот что с размером вот этой не менее простой структуры?

struct derived : public base {
    char m_mask;
};

А вот тут сюрприз!

Результат зависит от компилятора: для VC++ получается 24 байта (и в этом есть некоторая логика), тогда как для GCC и clang -- 16 байт (и в этом также есть логика).

Но лично мне представляется, что логика GCC/clang как-то логичнее. Тогда как поведение VC++ стало неприятным открытием. Можно даже сказать как серпом...

Посмотреть самому можно на godbolt: https://godbolt.org/z/sbo8jPcxE

вторник, 20 августа 2024 г.

[prog.c++] Вынесу из комментариев на Хабре про дебилизм с std::launder и std::start_lifetime_as

В комментариях к статье на Хабре зашел разговор об уместности использования std::launder и std::start_lifetime_as.

И, насколько я смог понять из разговора, вот в такой ситуации у нас нет сейчас простого и понятного способа в точке (1) сделать каст указателя к Demo*:

#include <iostream>
#include <functional>
#include <new>
#include <cstddef>
#include <cstring>
#include <memory>

struct Data {
    int _a{0};
    int _b{1};
    int _c{2};
};

using CreateFn = std::function<void(std::byte*)>;

void make_and_use_object(CreateFn creator) {
    alignas(Data) std::byte buffer[sizeof(Data) * 3];
    std::byte * raw_ptr = buffer + sizeof(Data);
    creator(raw_ptr);
    Data * obj = std::launder(reinterpret_cast<Data *>(raw_ptr)); // (1)
    std::cout << "obj: " << obj->_a << ", " << obj->_b << ", " << obj->_c << std::endl;
}

int main() {
    make_and_use_object([](std::byte * ptr) {
        new(ptr) Data{._a = 1, ._b = 2, ._c = 3};
    });
    make_and_use_object([](std::byte * ptr) {
        Data data{ ._a = 25, ._b = 16, ._c = 890};
        std::memcpy(ptr, &data, sizeof(data));
    });
}

Суть в том, что когда make_and_use_object вызывается с первой лямбдой и по указателю ptr новый объект Data создается через placement new, то затем raw_ptr нельзя просто так скастить к Data*. Тут требуется именно std::launder. Ну вот требуется и все. Иначе UB.

Тогда как при использовании второй лямбды в точке (1) вообще не нужно вызывать ни std::launder, ни std::start_lifetime_as. Вроде как все дело в том, что std::memcpy неявно начинает время жизни нового объекта. И результирующий указатель можно просто скастить к Data* и все.

Но, допустим, что во второй лямбде я не использую std::memcpy, а заполняю переданный мне буфер собственной функцией побайтового чтения из COM-порта. Что-то вроде:

 make_and_use_object([](std::byte * ptr) {
     com_port_reader reader;
     reader.init();
     while(reader.has_data()) {
         *ptr = reader.read_next_byte();
         ++ptr;
     }
 });

Естественно, ничего из этого в стандарте не описано и компилятор понятия не имеет, начинается ли здесь какой-нибудь lifetime или нет.

Соответственно, что делать в make_and_use_object после завершения работы лямбда-функции?

Если содержимое объекта внутри буфера было сформировано побайтовым чтением из COM-порта, то std::launder не поможет, тут нужен как раз std::start_lifetime_as (который завезли в C++23, но который, если не ошибаюсь, пока нигде не реализован).

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

Хотя, казалось бы, C++ -- это такой язык, в котором подобные операции должны были бы делаться легко и просто. Ага, щаз... 🥴 С подачи компиляторописателей в язык напихали столько UB, что подобный кастинг указателя стал отнюдь не простым.

Для полноты картины: там же в комментариях указали на наличие пропозала от Антона Полухина. Смысл этого пропозала -- отказаться от необходимости использовать std::launder вот в таких простых ситуациях:

alignas(T) std::byte storage[sizeof(T)];
auto* p1 = ::new (&storage) T();
auto* p2 = reinterpret_cast<T*>(&storage);
bool b = p1 == p2;  // b will have the value true.

Если этот пропозал примут, то ситуация окажется совсем веселая:

  • до C++26 подобный кастинг без std::launder будет считаться UB. Т.е. если вы пишете под C++17 или C++20, то должны использовать std::launder, иначе в вашем коде формальный UB;
  • начиная с C++26 это уже не UB и можно std::launder не писать.

А теперь представим проект (какую-нибудь библиотеку, вроде RapidJSON), который должен собираться и под C++14, и под C++17, и под C++20, и под C++23, и под C++26. И как в таком проекте быть со всеми этими std::launder и std::start_lifetime_as? Кроме как прятать подобные фокусы за фасадом макросов мне ничего в голову не приходит.

Но пусть даже у нас есть набор нужных вспомогательных макросов... Вернемся к самому первому примеру в посте. Как там понять, требуется ли std::launder, std::start_lifetime_as или же вообще ничего не требуется?

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

Собственно, чего хотелось бы иметь:

  • чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A, создали новый объект A (или даже какой-то отнаследованный от него B -- пример), старые указатели "протухли", нужно их "отмыть" через std::launder. Все. Больше ни для чего std::launder не нужен;
  • чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.

И строго так, без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.

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

PS. Да, я в курсе, что практику с неявным началом времени жизни при использовании ряда функций (вроде malloc или memcpy) в C++20 ввели для того, чтобы узаконить говнокод, написанный в древности или даже вообще на чистом Си (как в случае с GCC -- сперва это был Сишный код, а потом еще стали компилировать как C++). Но ведь C++ все равно потихоньку отказывался от атавизмов, например, ключевое слово auto кардинально поменяло свой смысл, а ключевое слово register сейчас нельзя использовать по его первоначальному назначению. Так что лично для меня этот аргумент из категории "ну такое себе". А если какие-то комитетчики настаивают на то, что этот говнокод нужно оставить как есть и сделать легальным (да еще и за счет умолчаний, про которые мало кто знает), то хочется сказать таким комитетчикам: ну так оставьте легальным и тот говнокод, в котором std::launder не используется.

PPS. На всякий случай ссылки на посты двухгодичной давности, в которых обсуждалась проблематика std::launder: "Продолжение темы про передачу C++объектов через shared memory. Промежуточные выводы" и "В склерозник: ссылки на тему std::launder"

воскресенье, 31 марта 2024 г.

[prog.c++] Оказывается, для VC++ недостаточно ключа -std для того, чтобы получить актуальное значение __cplusplus

Если вы, как и я, думали, что указав компилятору ключ -std:c++20 вы автоматически получите корректное значение __cplusplus, то вы заблуждаетесь :(

Кроме ключа -std:c++20 нужно еще и ключ -Zc:__cplusplus указать. Цинк:

$ cat t.cpp
#include <iostream>

int main() {
        std::cout << __cplusplus << std::endl;
}

$ cl -EHsc -nologo -std:c++20 t.cpp
t.cpp

$ ./t.exe
199711

$ cl -EHsc -nologo -std:c++20 -Zc:__cplusplus t.cpp
t.cpp

$ ./t.exe
202002

Рецепт найден здесь.

Что тут остается сказать кроме "Вот уроды!"...

среда, 20 марта 2024 г.

[prog.cmake] Попытался заглянуть в документацию по CMake. Пригорело. Нехило так пригорело :(

Я вот правда не понимаю, как этот кусок говна умудрился стать стандартом де-факто. И еще я не понимаю вот чего: нам, си-плюс-плюсникам, что, мало сложностей и несуразностей в самом C++? Мы так мало набили шишек на граблях C++, что радостно взялись скакать еще и по граблям CMake? Не, реально?

Вообще мне очень неприятно, что и меня, и разработчиков CMake называют программистами. А мне не хочется быть коллегой таких горе-разработчиков. Они уже упороты!

PS. Пожалуйста, не нужно спрашивать у меня чем заменить CMake. Уже почти 20 лет пользуюсь собственной системой сборки, написанной на Ruby. Когда-то даже прикладывал усилия чтобы продвинуть свой лисапед в массы, но не преуспел. На этом считаю свой долг по улучшению C++ной экосистемы полностью выплаченным.

вторник, 19 марта 2024 г.

[prog.c++.kill'em-all] Еще пример C++ного кода от которого у меня изрядно подгорает

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

enum class status { not_started, started, shutting_down, stopped };

#if defined(PLATFORM_WINDOWS)
[[nodiscard]] const wchar_t * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return L"not_started";
    case status::started: return L"started";
    case status::shutting_down: return L"shutting_down";
    case status::stopped: return L"stopped";
  }
}
#else
[[nodiscard]] const char * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return "not_started";
    case status::started: return "started";
    case status::shutting_down: return "shutting_down";
    case status::stopped: return "stopped";
  }
}
#endif

вот не надо, пожалуйста.

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

Сделайте хотя бы так:

// Эта кухня должна жить в отдельном заголовочном файле.
#if defined(PLATFORM_WINDOWS)
  using platform_char_type = wchar_t;
  #define STRING_LITERAL(str) L##str
#else
  using platform_char_type = char;
  #define STRING_LITERAL(str) str
#endif

enum class status { not_started, started, shutting_down, stopped };

[[nodiscard]] const platform_char_type * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return STRING_LITERAL("not_started");
    case status::started: return STRING_LITERAL("started");
    case status::shutting_down: return STRING_LITERAL("shutting_down");
    case status::stopped: return STRING_LITERAL("stopped");
  }
}

четверг, 14 марта 2024 г.

[prog.flame] Самодокументирующися код против документированного, наглядно

Намедни в LinkedIn поиронизировал на счет "самодокументирующегося кода". В очередной раз 😎

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

Подумал, что лучше одна иллюстрация лучше тысячи слов. Поэтому вот пример кода из реального проекта. Изменены только названия сущностей, все комментарии оставлены как есть. Плюс изъяты фрагменты, не относящиеся к самой иллюстрации.

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

Под катом оригинальный фрагмент с комментариями. Такими, какими они и были написаны при разработке. Но сперва этот же фрагмент, но вообще без комментариев. Именно так "самодокументирующися" код и выглядит, по моему (не)скромному опыту.

class DefaultThreadPoolScheduler final : public Scheduler
{
   ...

private:
   struct WorkerData
   {
      std::mutex _lock;
      std::condition_variable _wakeupCondition;

      Scheduler::TaskUniquePtr _taskToRun;

      bool _shutdownInitiated{ false };
   };

   using WorkerDataContainer =
      std::vector<std::reference_wrapper<WorkerData>>;

   std::latch _allWorkersStartedLatch;

   std::mutex _lock;

   TasksContainer _tasksQueue;

   ThreadPool _threadPool;

   WorkerDataContainer _availableWorkers;

   bool _shutdown{ false };
};

void DefaultThreadPoolScheduler::doWork() noexcept
{
   WorkerData thisWorkerData;

   {
      std::lock_guard schedulerLock{ _lock };
      _availableWorkers.push_back(std::ref(thisWorkerData));

      _allWorkersStartedLatch.count_down();
   }

   bool shutdownIntitiated{ false };
   while( !shutdownIntitiated )
   {
      std::unique_lock workerLock{ thisWorkerData._lock };

      if( TaskUniquePtr taskToRun = std::move(thisWorkerData._taskToRun); !taskToRun )
      {
         shutdownIntitiated = thisWorkerData._shutdownInitiated;
         if( !shutdownIntitiated )
         {
            thisWorkerData._wakeupCondition.wait(workerLock);

            shutdownIntitiated = thisWorkerData._shutdownInitiated;
         }
      }
      else
      {
         workerLock.unlock();
         taskToRun->run(Scheduler::RunCondition::Normal);
         completeTaskThenTryGetNext(std::move(taskToRun), thisWorkerData);
      }
   }
}

четверг, 29 февраля 2024 г.

[prog.c++.flame] Вот пример того, что мне сильно не нравится в C++

Попалась мне несколько дней назад старая статья на тему хитрых трюков в C++: "NON-CONSTANT CONSTANT-EXPRESSIONS IN C++". Да, я знаю, статья не новая, но вот такой я медленный газ... 🙁

В статье показывается некий трюк, который типа должен позволить сделать так:

int main () {
  constexpr int a = f ();
  constexpr int b = f ();

  static_assert (a != b, "fail");
}

Т.е. два вызова одной и той же constexpr-функции в compile-time должны дать разный результат.

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

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

Хочется привести такую аналогию: возможно вы замечали, что временами при вставке штепселя в электрическую розетку возникает искра (и при извлечении такое случается). Иногда искр меньше, иногда больше. Насколько я знаю, этот эффект физиками давно изучен и он лежит в основе электроискровой резки металлов. Ну да не суть. Суть в том, что вот кто-то обнаружил этот эффект и начал экспериментировать: штепсели с разным диаметром контактов, скорость вставки/извлечения, угол под которым все это выполняется и т.д. А потом написал статью, что типа если вы будете делать вот так, то у вас гарантированно будут искры и искры эти будут наиболее яркими. А потом кто-нибудь еще и попытается извлечь какую-нибудь "пользу" из такого "открытия"?

Нормальные люди прочитав о таком, скорее всего, просто покрутят пальцем у виска.

Тогда как в мире C++ вот такое вот "открытие" привлекает внимание и ведет к продолжению подобных поисков и экспериментов. И это считается нормальным. Более того, такие вещи обсуждаются. Возможно, кто-то даже решается применить что-то подобное в своей работе. А потом это вырастет в какую-то продвинутую технику программирования на C++, по типу случайно открытого в середине 1990-х метапрограммирования не шаблонах...

Мне это не нравится. Все-таки хочется, чтобы С++ развивался в сторону удобного, практичного и предсказуемого инструмента, где для решения задач используются специально спроектированные возможности языка программирования, а не это вот все 🙁

Но самый цимес обнаружился при попытке проверить представленное в статье решение 😅

Оказалось, что само решение (в коротком его варианте), работает только в VC++.

Под GCC работает только то длинное решение, которое типа должно обходить баг в clang-е. А вот короткое решение не работает.

Тогда как в clang-е не работает вообще ни короткое, ни длинное.

Ну и, спрашивается, нахера весь этот огород было городить, если он "работает" только на одном из компиляторов? Так и хочется вспомнить старое, но актуальное: "Не выёбывай умничай" 🤬

Павбывавбы.

вторник, 6 февраля 2024 г.

[prog.c++.kill'em-all] C++ный код, от которого у меня изрядно подгорает

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

error_code use_resource(resource_id res_id) {
  if(auto r = first_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  ...
  dispose(res_id);
  return error_code::ok;
}

Думаю, что несложно догадаться, что именно триггерит: это обилие вызовов dispose.

Я могу простить тот факт, что в use_resource передается голый дескриптор ресурса, а не какая-то RAII-обертка вокруг него.

Ну мало ли, бывает. Может эта функция вообще как extern "C" описана и предназначена для того, чтобы ее вызывали из Си-шного кода. Или же это часть древнего проекта и первоначально use_resource была написана еще в конце 1980-х, а сейчас ее просто дорабатывают не имея возможности поменять все 100500 мест в старой кодовой базе, где она вызывается именно вот так.

Но блин, почему нельзя сделать RAII обертку уже внутри use_resource?

Хотя бы подобным образом:

error_code use_resource(resource_id res_id) {
  struct resource_disposer {
    resource_id m_id;
    resource_disposer(resource_id id) : m_id(id) {}
    ~resource_disposer() { dispose(m_id); }
  } disposer(res_id);

  if(auto r = first_operation(res_id); r != error_code::ok) {
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    return r;
  }
  ...
  return error_code::ok;
}

Причем реализация такого `resource_disposer` -- это вообще C++98. Таким подходом можно пользоваться уже больше двадцати пяти(!!!) лет без оглядки на версию компилятора. В современном C++ можно было бы найти еще несколько способов достижения той же самой цели (хотя бы finally из GSL), более лаконичных.

На эту тему подобной "очистки" ресурсов я уже неоднократно писал. Вот, например, от 2015-го года (уже почти десять лет как!!!): раз и два. Но, как я смотрю, время идет, а криворуких программистов недоучек меньше не становится.

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

Вероятно, C++ программистов нужно начинать учить с идиомы RAII. А уже все остальное -- потом.

Ну а Си-программистов, по аналогии, нужно начинать учить с идиомы goto err (или goto cleanup). Даже не смотря на то, что goto -- это зло. Как и чистый Си, впрочем ;)


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

вторник, 9 января 2024 г.

[prog.c++] Оказывается, в современном C++ нельзя взять и сложить std::string с std::string_view...

На пятый год работы с C++17, в котором std::string_view появился, "Зоркий глаз" (т.е. я) заметил, что в C++ пока нет версии operator+ для случая std::string и std::string_view :(

Поэтому ни в C++17, ни в C++20, ни, подозреваю, в C++23, не получится написать так:

std::string f(std::string_view a, std::string_view b) {
  using namespace std::string_view_literals;
  return std::string{"Expected value: "} + a + ", actual value: "sv + b;
}

Но есть пропозал. И, может быть, нам повезет и в C++26 эта фича в языке таки появится. А может только в C++29...

Если честно, то я, мягко говоря, в шоке.


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