Как стать автором
Обновить
87
0.2
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

Отправить сообщение

В чем причина UB?

Насколько я понимаю, причина в том, что компилятор C++ должен уметь отслеживать начало и завершение времени жизни объекта. Например:

some_struct s1; // Здесь очевидное начало lifetime для объекта.
some_struct * p1 = new some_struct{}; // Здесь еще одно очевидное начало lifetime.
some_struct * p2 = new(&s1) some_struct{}; // Здесь очевидное начало жизни для p2.

В случае, когда мы тупо делаем reinterpret_cast или C-style case, время жизни ни для какого объекта не начинается.

Ну, блин. Это не UB. Это просто явный косяк.

Вы не за то зацепились. Проблема не в значении 53. Проблема в приведении типа. В чистом Си вот это не UB:

some_struct * p = (some_struct *)(some_ptr + some_offset);

тогда как в C++ этот же код:

some_struct * p = (some_struct *)(some_ptr + some_offset);

будет иметь UB. Этот UB не эксплуатировался компиляторами до C++20 (скорее даже до С++23), тогда как начиная с C++23 никаких гарантий по этому поводу уже нет. Не поставил std::start_lifetime_as, ну значит ССЗБ.

В данном конкретном случае явный кастинг может стать причиной UB только потому, что компилятор начинает неправильно что-то там оптимизировать.

Оптимизатор не может стать причиной UB. Оптимизатор может воспользоваться оставленным пользователем UB. Например, в C++ (если я не запутался в нововведениях), вы не можете просто так написать код вроде:

char data[1024];
read_data(data);
some_struct * payload = reinterpret_cast<some_struct *>(data + 53 /*пропустили заголовок*/);

В чистом Си можете, а в C++ начиная с C++23 для этих целей следует применять std::start_lifetime_as.

Потому что порядок следования байт и выравнивание мы устанавливаем руками одинаковое с обеих сторон.

У "выравнивания" есть две составляющие в данном вопросе:

Первое: упаковка данных внутри буфера. Так, если у вас структура вида:

struct header {
  uint32_t _magic_number;
  uint8_t _version;
  uint16_t _fields;
  uint64_t _flags;
};

то без #pragma pack для вашей структуры в зависимости от настроек компилятора между _version и _fields может быть "дыра".

Второе: выравнивание адресов для вашей структуры на принимающей стороне. Тот самый alignas из современного C++. Без оного фокус вида:

char data[sizeof(header)];
read_data(data);
header * h = (header *)data;
if(0xDEADBEAF == h->_magic_number) // OOPS!

может аварийно завершится на некоторых платформах в точке "OOPS".

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

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

Вот поэтому критичные по скорости куски кода писали на С.

Если вы думаете, что в чистом Си нет UB, то вам нужно прочитать хотя бы вот эту серию статей: https://habr.com/ru/articles/341048/ (это первая, в ней ссылки на остальные).

Если очередная версия компилятора превращает работающий код в ХЗ что, то нужен ли такой компилятор?

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

ЕМНИП, одним из UB, в частности, было то, что до C++20 вот такой вот код:

alignas(DataHeader) char buffer[sizeof(DataHeader)];
read_data(buffer);
DataHeader * header = reinterpret_cast<DataHeader *>(buffer);

содержал в себе UB.

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

Эт да, но это одна из самых очевидных проблем, которую легко обойти, если делать преобразование представления прямо "по месту". Веселее с неочевидными, когда люди, например, не знают про выравнивание данных и директивы вроде #pragma pack.

И ради чего все это? Вот чтобы что?

Чтобы в коде не было UB и чтобы очередная версия компилятора не превратило результат трансляции вашего cast-а в ХЗ что.

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

Если у вас свежий C++, то в C++20 уже были сделаны некоторые послабления.
А в C++23 завезли std::start_lifetime_as.
Проблем с выравниванием данных это не решает (тут вы правы на счет переносимости), но хотя бы избавляет код от UB.

на приктике такие конструкции не то что никому не нужны , они будут мешать. Поэтому так писать никто не будет, поэтому вы и стесняетесь объяснять в чем смысл вашего кода, потому что объяснения вам самому покажут какая там ерунда написана

Жаль, что здесь есть только две оценки для комментария: +1 и -1.
Очень, очень не хватает оценки facepalm. Хотя здесь бы более уместной была бы double-facepalm.

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

Раз пошла такая пьянка, то по моим личным ощущениям (моим личным, это важно) C++ники вообще никому не нужны.

Это пойдёт как уже значимая выборка? 3-5%.

Да.

Но при крайне слабом контроле качества кода это всё превращается в невнятную кашу.

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

Так что предполагаю, что не шаблоны тому виной. Они лишь катализатор (в плохом смысле этого слова).

Как бы да, и я стараюсь об этом прямо сказать.

Но, позволю себе немного растечься мыслею. Сама статья весьма специфична и в ней речь идет о (кмк) сравнительно узкой сфере задач, решаемых на C++. Тем не менее, она актуальна в том плане, что автор статьи рассказывает о вещах, которые появились a) недавно (так мне показалось) и b) как способ преодолеть ограничения, достигнутые при применении универсальных языков (C++ в частности). Т.е. сперва задачи из предметной области автора статьи решались на C++ (и конкурентов практически не было), но времена меняются и сейчас вне C++ можно достичь результатов лучше, чем на C++.

Здесь все OK. С поправкой на то, что не нужно специфику узкой области автора статьи экстраполировать на вообще весь C++ и сферы его применения (что, кмк, происходит в комментариях, автор статьи такой попытки и не делал).

Но вот появляется персонаж, который в очередной раз (ну вот реально он не впервые прибегает с RPG в комментарии к статьям про C++) говорит: "А вот RPG в моих задачах рвет C++ как тузик грелку". И это не имеет отношения к поднятой в статье теме просто потому, что RPG специально затачивался под такие задачи, и рвет он не только C++, но и вообще все за исключением COBOL-а (с той же самой платформы).

Мало того, что данный персонаж со своим RPG уже поднадоел, так еще и непонятно какие выводы из его историй стоит сделать (и это принципиально отличает обсуждаемую статью). Ближайшая аналогия: если вам нужны регулярки, то используйте готовые regex-а, а не костыльте их самостоятельно. Ну как бы да, спасибо, Кэп.

Ну так о чем и речь.

Тогда повторю свой вопрос: а при чем здесь C++?

Зачем вы именно C++ противопоставляете "наиболее эффективному инструменту под задачу"?

Я сравниваю сравнимое.

Да я тоже могу на C++ сделать DSL для описания регулярного выражения, а потом сравнить его результат с выхлопом, скажем, re2c. Или сделать на C++ вручную bottom up LR(1) парсер и сравню его с bison-ом.

Не, ну а чё? Сравнимое же.

А потом еще и буду говорить, но вот когда нужно делать обработку 2D изображений, тогда, конечно же, С++ круче, чем re2c и bison вместе взятых.

Получится как у вас с вашим любимым IBM i.

Я могу понять, когда люди сравнивают C++ и, скажем, Java для реализации проектов типа NetBeans или Eclipse. Когда компонентная архитектура, плагины от разных разработчиков. И хочется, чтобы все это работало и быстро, и надежно.

Мало кто толком понимает, как работает эта кухня, поэтому большинство лишь применяет то, что сделано "продвинутыми программистами"

А на какого размера выборке основываются ваши наблюдения о "мало кто толком понимает..."? Вы же вроде как чуть ли не в одиночку работаете.

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

Во-первых, тут бы хотелось бы пруфов. Но, сюрпрайз, сюрпрайз, их не будет. Немного предсказуемо, да.

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

Допустим, у вас 10KLOC и вы в состоянии оценить качество кода для семейства x86 процессоров. А если добавить сюда еще и пару-тройку ARM-ов? А если еще и e2k? А если еще что-нибудь?

А для проекта в 100KLOC вы в состоянии глобальное качество отслеживать?
А для проекта в 500KLOC?
А для проекта в 1MLOC?
И т.д.?

Может для проекта хотя бы в 100KLOC будет уже свой набор требований, в которых требования к производительности уже не будут приоритетом №1 (а если и будут, то не ко всему коду, а к отдельным его кускам)? Например, там будут иметь значения такие вещи, как обеспечение корректности, повторное использование и отсутствие копипасты с ее проблемами. Как раз те вещи, для обеспечения которых шаблоны (включая многоэтажные) хорошо себя зарекомендовали.

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

Посмотрите эту ветку обсуждения: https://habr.com/ru/articles/811151/comments/#comment_26776101
Я там основную претензию высказал -- когда начинается декларация типов в Python, исходная динамическая типизация прощается с нами.

А хотелось бы перенести проверки на этап компиляции.

Да. Но в статике хотя бы часть проблем с типизацией выявляется во время компиляции, а не в рантайме.

Почему же?

По факту.

Без декларации типов не заработает.

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

Вот только динамическая типизация, о которой речь шла выше, где-то по дороге закончилась.

И, честно говоря, не совсем понимаю вашу позицию - вы что пытаетесь доказать?

Я пытаюсь доказать, что когда вы сравниваете RPG с C++ в своих задачах, то вы напрасно акцентируетесь на C++. Т.к. на место C++ в этом сравнении можно подставить хоть Java, хоть C#, хоть Kotlin, хоть Scala. Ничего не поменяется.

И кому?

Вам.

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

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

Не подставляете же вы именно потому, что в бэкграунде у вас C++. Была бы Java, вы бы точно так же говорили про Java.

Если и сейчас до вас не дойдет...

Информация

В рейтинге
2 162-й
Откуда
Гомель, Гомельская обл., Беларусь
Зарегистрирован
Активность