LINUX.ORG.RU

В C++ становится меньше UB

 , ,


2

5

Привет, чят!

В общем, сабж. Начиная с C++26 тривиальные бесконечные циклы больше не будут неопределённым поведением.

Пример такого цикла:

int main() {
  while(true) ;
}

При этом, если в теле цикла содержится выражение или условие цикла не является константным выражением, приводимым к true, то такой цикл всё ещё будет вызывать неопределённое поведение программы:

int main() {
  // UB
  while(true) {
    (void) "Hello LOR!";
  }
}
int main() {
  // UB
  bool done = false;
  while(!done) {}
}
int main() {
  // Тоже UB
  while (true) if constexpr (false) break;
}

И так далее. Как всегда, в C++ что-то починили, по сути ничего не починив. Так и живём, чят.

Ссылка с подробностями: https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-infinite-loops



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

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

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

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

Кушанье ЦПУ не рассматривается стандартом, так что имеем что имеем.

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

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

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

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

шланг, мсвц, гцц. У настоящих сишников настоящий си - это условный гцц версии 2.97, компилирующий под определённую платформу. Всё остальное не си. Даже стандарт си не си.

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

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

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

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

Мне было бы интересно посмотреть, какие именно оптимизации срабатывают при изнасиловании кода с while(true); и насколько они ускоряют код. Потому что, мне что-то кажется, их либо просто нет, либо они настолько незначительны, что их можно выкинуть.

yorshka
() автор топика
Ответ на: комментарий от AndreyKl

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

БЕСКОНЕЧНЫЙ
ЦИКЛ

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

Не возникает ли желания лишний (нет) раз назвать кого-нибудь дегенератами?

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

Это то известно для чего: чтобы компилятор мог выкидывать произвольные циклы без «побочных эффектов», не пытаясь перед этим проверить, есть ли у них конец (предполагая что он всегда есть). Ну то есть какое-нить

{
  uint x = 1;
  while(x!=3625364377) x = x*3732351441;
} /* тут x исчезает из доступности и значит его значение ни на что не влияет */
А именно в С++ за это держатся потому, что ожидают появления таких циклов при раскрытии шаблонов (ну, то есть программист не виноват, он писал код в общем виде, а в частном случае какой-то цикл оказался не нужен и мы его выкинем). Ситуация, по-моему, несколько маловероятная, ну и портить ради оптимизаций осмысленный код в любом случае плохая идея.

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

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

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

пытаюсь обдумать эту мысль.

на ум приходит «ну как бы да, бесконечных циклов не бывает, по крайней мере у нас в реальности»

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

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

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

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

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

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

ну и сугубо чтобы показать что красивый аргумент - не всегда сильный и тем более верный:

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

материальное ничто

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

Лингвистический парадокс. Я этим хочу сказать что, то что слова кто то написал и в общем то у них есть смысл, и, даже то, что люди их пишут и стыкуют друг с другом не просто так (ну, в идеале) - само по себе совершенно ничего не доказывает. Ещё примерчик, мне особенно симпатичен почему то.

Может ли всепробивающее ядро пробить ничем не пробиваемую стену?

Предлагаю считать сеанс медитации открытым :)

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

Может ли всепробивающее ядро пробить ничем не пробиваемую стену?

А вот тут у нас возникает состояние гонки. Гонки снаряда против брони. В конце концов эта гонка остановится, когда и первое, и второе станут неприменимым в реальности. «Ничем непробиваемое» на каком-то этапе становится «пробиваемым хотя бы одним типом орудия», появляется новое «ничем непробиваемое», и так по кругу…

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

просто взять, написать два слова: материальное ничто

А рядом нужно нарисовать стрелочный индикатор, проградуированный от «программирование» до «шизофазия» и на нем стрелочку, отклонившуюся на какой-то угол. Только честно, ладно?

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

А, вы считаете, что тут какая-то особенная обработка кода? Не, просто компилятор видит, что в коде UB, и выкидывает его, вот и всё. В детали - есть тут ускорение или нет - вдаваться ему не надо.

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

с одной стороны решил выкинуть цикл, а с другой - «не заметить» что конец функции после этого тривиально достижим

Если конец функции достижим после выкидывания кода с UB, с точки зрения стандарта это значит, что функция никогда не вызывается. Поэтому выкидывание всего хвоста функции вполне адекватно.

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

А, вы считаете, что тут какая-то особенная обработка кода?

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

Как упомянул @firkax выше, это может происходить для оптимизации кода типа

{
  uint x = 1;
  while(x!=3625364377) x = x*3732351441;
}

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

Не, просто компилятор видит, что в коде UB, и выкидывает его, вот и всё.

Нет, это не так работает.

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

Нет, это не так работает.

Именно так и работает. Допустим, такой код:

int a = *p;
if (!p) {
  ...
}

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

Бесконечный цикл классифицирован как UB. Можно его выкинуть, выкинуть всё, что после него (ты же не расчитывал всерьёз, что твой код после бесконечного цикла когда-то будет исполняться?), а так же весь код, который безусловно доводит исполнение программы до этой точки.

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

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

Нет, там другая логика. Если указатель был разыменован, значит он 100% не NULL и проверка ненужна.

Бесконечный цикл классифицирован как UB.

Да, это безусловно так, но…

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

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

yorshka
() автор топика
Ответ на: комментарий от yorshka

Если указатель был разыменован, значит он 100% не NULL и проверка ненужна.

Если бы не было понятия UB при разыменовании NULL, то эта логика не работала бы.

я всё же склонен считать

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

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

Если указатель был разыменован, значит он 100% не NULL и проверка ненужна.

Если бы не было понятия UB при разыменовании NULL, то эта логика не работала бы.

Ну, да. Всё так.

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

Почему? Ты в этом уверен? Почему в языках без UB (в том же Rust) можно проводить оптимизации без подобного треша и получать не менее производительный код?

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

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

yorshka
() автор топика
Ответ на: комментарий от yorshka

Почему в языках без UB (в том же Rust) можно проводить оптимизации без подобного треша и получать не менее производительный код?

В расте полно UB, если ты пишешь в unsafe. В safe указатель заведомо валидный, и проверять его не потребуется.

есть данные

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

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

Эта оптимизация просто попадает под общую логику «видим UB - выкидываем». И формально она бесконечно ускоряет код.

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

шланг, мсвц, гцц. У настоящих сишников настоящий си - это условный гцц версии 2.97, компилирующий под определённую платформу. Всё остальное не си. Даже стандарт си не си.

Мне интересно, Green Hills тоже так делает или он более лучший си? К сожалению с места, где он применялся, уже уволился, не проверить.

ant1
()
Ответ на: комментарий от thesis

А рядом нужно нарисовать стрелочный индикатор, проградуированный от «программирование» до «шизофазия» и на нем стрелочку, отклонившуюся на какой-то угол. Только честно, ладно?

Бери шире: программирование это всего лишь раздел математики, так что нумеруй сразу от «математика» до «шиза». Некоторые вот детям 10 лет подряд втирают про какие-то 2D пространства, плоскости и точки. Потом эти дети, уверовав в разные D, с умным видом тащат в физику 4D, 5D и 125D.

ant1
()
Ответ на: комментарий от unC0Rr

В расте полно UB, если ты пишешь в unsafe.

Для бесконечного цикла требуется unsafe? Нет, не требуется.

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

Отлично. Есть другие исследования? Потому что все эти пляски про UB абсолютно бессмысленны, если никто не проверял эффективность этих оптимизаций за пределами микробенчмарков.

Эта оптимизация просто попадает под общую логику «видим UB - выкидываем». И формально она бесконечно ускоряет код.

В таком случае можно любую программу с доказанным UB выкинуть, заменив на int main(void) { return 0; }, но компиляторы так почему-то не делают.

yorshka
() автор топика
Ответ на: комментарий от yorshka

В таком случае можно любую программу с доказанным UB выкинуть, заменив на int main(void) { return 0; }, но компиляторы так почему-то не делают.

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

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

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

В смысле? Достаточно UB в одном единственном файле. Дальше вся сборка прерывается и возвращается заранее сделанный пустой бинарик. Это наоборот ускорит сборку.

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

Короче, C++ стал жертвой собственных популярности и долговечности.

yorshka
() автор топика
Ответ на: комментарий от yorshka

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

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

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

ну ты придираешься к формулировкам

Слушай, ну я в том сообщении тебе конкретный пример привел, касающийся observable size effect в виде завершения программы.

Ты мои слова проигнорировал и написал странных вещей: «вот смотри, программа зависла в цикле. это значит что никогда дальше чем цикл она не попадёт. т.е. она всегда будет на этом месте и это и есть её конец».

Тебе на этот счёт разные люди уже выше пытались объяснять по-разному, но я попробую еще раз тоже:

Абстратная машина/модель исполнителя включает в себя НЕ ТОЛЬКО синхронное локальное состояние потока.

Существуют еще асинхронные события. Поток может на синхронном уровне находиться в бесконечном цикле, но при этом обрабатывать сигналы. В Windows приблизительный аналог сигналов называется APC (asynchronous procedure call). Поток НЕ ЗАВЕРШЕН. Он функционирует и выполняет код, его поведение наблюдаемо. (Надеюсь, что до того, чтобы считать вызовы сигналов за UB, в этом обсуждении речь не дойдёт?)

Далее, если мы говорим о многопоточном исполнителе, у этого исполнителя обязательно есть эксплицитное состояние: поток либо завершен к понятиях поточного API, либо нет. Это то, что ты можешь наблюдать в API любой ОС, без всякой абстрактной математики.

Поток, который находится в бесконечном цикле – очевидно НЕ завершен, статус terminated для него не выставлен. Это тоже наблюдаемый эффект.

Ты же взял частную проблему из математической формализации предметной области и придумал вокруг неё какую-то оторванную философию.

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

Я вот не уверен что 100% неумышленное. Иногда кажется, что шлангоавторы специально подстраивают пакости использующим их компилятор программистам, из желания «научить неверных/„неграмотных“ правильно программировать». Хотя скорее всего, если такая мотивация где-то и имеется, то в подавляющем меньшинстве случаев, а в большинстве это просто концентрация на разово сформулированной цели разработки очередной «фичи» оптимизатора и полное наплевательство на побочные эффекты своих решений до тех пор, пока они не начинают расходиться с бюрократическими бумажками.

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

Кстати, вот фрагмент обсуждения аж от 2010-го года, который вполне разумно указывает, что программы полагаются на вход в бесконечный цикл как на «дальше не пойдёт», и «нет программ, которые полагались бы на то, что компилятор выбросит for(;;);»: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1509.pdf

Нарушающим conformance в такой ситуации оказывается компилятор, а не программа.

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

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

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

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

В unix сигнал будет доставлен потоку только на переключении контекста

И какое это отношение имеет к абстрактной исполнительной машине Си/Си++? Никакого. Это деталь реализации ОС.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void sigint_handler(int signo)
{
    (void)signo;
    const char msg[] = "\nSIGINT получен\n";
    ssize_t ignored = write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    (void)ignored;
    _exit(0);
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return EXIT_FAILURE;
    }

    while (true);
}
$ gcc 1.c && ./a.out 
^C
SIGINT получен
$ 

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

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

Однако насколько я понял из документации, Special User APC может быть точно так же доставлен на переключении контекста, как и сигнал в Linux.

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

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

Си/цпп компилируются по одному файлу. Если компилятор доказал, что UB всегда вызывается в одной функции, ему ещё надо проследить, всегда ли она вызывается, чтобы что-то выкидывать из вызывающего кода. Сверх этого, сигнатуру функции он всё равно обязан оставить в объектном файле, иначе будут проблемы с линковкой. В общем, все эти дополнительные анализы выливаются в увеличенное время компиляции ради совсем небольшого профита.

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

SIGINT получен

Тут не прав, признаю. Думал, что сигналы доставаляются только в TASK_INTERRUPTIBLE.

Special User APC может быть точно так же доставлен на переключении контекста,

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

И какое это отношение имеет к

А я не в рамках срача о бесконечном цикле.

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

Абстратная машина/модель исполнителя включает в себя НЕ ТОЛЬКО синхронное локальное состояние потока.

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

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

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

Си/цпп компилируются по одному файлу.

Это не важно. Сборка всего проекта прерывается, если один файл не скомпилировался. Добавить ещё один вариант ошибочного состояния для сборочной системы – вообще ни разу не проблема.

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

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

прерывать сборку ошибкой при нахождении UB в коде

Так UB - это не ошибка. Это состояние, которого валидная программа не достигает, и компилятор вправе предполагать, что программист предусмотрел это. Рассматривая ситуации с UB как невозможные, компилятор может делать выводы о значениях переменных и недостижимых путях исполнения, направляя оптимизацию с учётом этого знания. Ну, как выкидывание проверки указателя на NULL, если есть безусловное разыменование этого указателя выше или ниже по коду.

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

Это состояние, которого валидная программа не достигает, и компилятор вправе предполагать, что программист предусмотрел это.

Это в случае, если есть путь, по которому программа это состояние не достигает.

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

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

прерывать сборку ошибкой при нахождении UB в коде

Так UB - это не ошибка.

Вот в этой программе на Си есть UB и её сборка завершается ошибкой:

#include <stdio.h>

int main(void) { puts("Hello ); }

Серьёзно. В сишном стандарте написано, что это UB:

An unmatched ’ or " character is encountered on a logical source line during tokenization (6.4).

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

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

P.S. использую в примерах сишечку вместо плюсиков, потому что в плюсовом стандарте описание UB распихано по всему тексту, в сишном же есть отдельная секция, где оно всё удобно сгруппировано.

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

Скину прореженные цитаты от ИИ. Не со всем написанным согласен, равно как и не доверял бы без проверки всем перечисленым фактам. Но как разметка «для подумать» - представляется интересным. На самом деле там таких разделов ИИ сгенерировал не 2, а 6, но вы ж всё равно читать целиком не будете.


1. Как концептуально трактовать UB

В треде спорят две модели, и беда в том, что участники их смешивают:

Модель А: «битовая машина» (позиция firkax/ckotctvo). Программа исполняется на реальном железе с реальной ОС. «Не определено» означает лишь «стандарт не описывает, что именно произойдёт, и не документирует это». Что-то всё равно произойдёт — и это «что-то» определяется процессором и ОС, куда и надо идти смотреть. В этой модели компилятор обязан транслировать код как написано, потому что у исполнения есть единственный физический смысл.

Модель Б: «абстрактная машина» (официальная позиция, позиция wandrien). Стандарт описывает математическую модель исполнения. Состояние, помеченное как UB, вне модели — это не «поведение какое-то», а «поведения нет». Отсюда и шизовый на вид вывод: раз в корректной программе такого состояния не бывает, оптимизатор вправе исходить из его невозможности.

Ключевое, что тред не артикулировал: эти свободы — разные. Из «стандарт не накладывает требований» логически следует только право (А) свободы исхода — реализация может выдать любой результат. Из этого никак не следует право (Б) свободы вывода — предполагать, что «это никогда не случится», и на этом основании переписывать соседний код. Именно (Б) порождает фоллтру в unreachable(), удаление проверок и «машины времени». Большая часть праведного гнева в треде направлена ровно против (Б), и этот гнев обоснован: шаг от «гарантий нет» к «можно считать невозможным» — это дополнительный комитетский/компиляторный выбор, а не следствие определения.

Исторически правы обе стороны частично: в эпоху X3J11/C89 UB действительно было ближе к «зависит от железа, не документируем» (это портативность-эскейп для экзотических архитектур), и фоллтру в соседнюю функцию никто не практиковал. Но текст стандарта давно говорит «no requirements imposed». Так что вопрос не «что значит слово», а как индустрия решила операционализировать отсутствие гарантий — и вот тут выбор был сделан в пользу (Б), причём без явного публичного обсуждения этого шага. Это, пожалуй, самая честная формулировка претензии треда.

(Академически это оформлено: исследование «Two viewpoints» по семантике C показало, что текст стандарта внутренне противоречив и переключается между моделями А и Б; проект Cerberus попытался построить непротиворечивую формальную семантику поверх реальной практики. Так что ощущение «стандарт шит белыми нитками» — не паранойя ЛОРа, а задокументированный академический факт.)


2. Нужно ли выделять специфированные кейсы из массива UB

Да — и комитет это уже делает, как раз в том направлении, которое предлагал в треде wandrien. Проверил:

  • В C23, а затем и в C++26 (предложение P2795R5, принято) появилась новая категория — «ошибочное поведение» (erroneous behavior): чтение неинициализированной переменной больше не UB. Код по-прежнему неправильный (и компилятору рекомендовано это диагностировать), но поведение определено: получается некоторое значение, и никаких «телепортаций» исполнения. То есть «дегенератский» кейс с провалом в соседнюю функцию из этого класса вычищен.
  • Тривиальные бесконечные циклы выведены из UB через P2809R3 — та самая новость.
  • Давно идёт перенос вещей из UB в implementation-defined/unspecified (знак остатка, размер указателей и т.п.).

Схема, которая вырисовывается и которая, на мой взгляд, правильная:

  1. Формально невыразимое (разыменование произвольного мусорного указателя в модели без адресов) — оставляем UB, но сводим к минимуму.
  2. «Очевидно ошибка, но исход разумный» — переводим в ошибочное поведение с рекомендацией диагностики.
  3. Платформозависимое — implementation-defined, документировать обязательно.

У этого есть цена, и её в треде не назвали: каждая такая экструзия связывает руки оптимизатору. Если бы выход за границы массива сделали «ошибочным поведением», компиляторам пришлось бы вставлять рантайм-проверки или трапы — и прощай векторизация половины числодробилок. Поэтому комитет экстрагирует только те кейсы, где риск безопасности высок, а выигрыш от оптимизации мал (неинициализированные чтения — из этой оперы; OOB-доступы — нет). Это здоровый критерий, и тред мог бы спорить о нём предметнее, чем о гомосексуализме.

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

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

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

unC0Rr ★★★★★
()

Раз уж про Си++ вроде всё содержательно обсудили в теме, скину фрагмент черновика из описания моего ЯП, как схожая проблема решается там.


Устранение недостижимых веток и завершимость циклов

Недостижимые ветки

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

select
case a == b: ...
case a == c: ...
case true: ...
case b == c: ... /* устраняется */
default: ...  /* устраняется */
end:select

if true then
	...
else
	...  /* устраняется */
end

if false then
	... /* устраняется */
else
	...
end

when false: ...; /* устраняется */

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

Аналогично устраняются циклы while, у которых условие является константным выражением, вычисляемым в false.

while false loop /* цикл устраняется целиком */
	...
end

Бесконечные циклы и вырожденные бесконечные циклы

В случае, если цикл:

  1. Записан как:
  • forever loop … end
  • while expr loop … end, где expr константно вычисляется в true
  • repeat … until expr, где expr константно вычисляется в false
  1. И при этом - после устранения недостижимых веток внутри тела цикла - из цикла нет выхода операторами exit или return, либо через секцию rescue, то такой цикл считается абсолютно бесконечным, имеет тип noreturn, а код после цикла — недостижимым.

Следует отметить:

  • Если из цикла нет выхода ни по условию цикла, ни оператором exit непосредственно в точку после цикла — то такой цикл рассматривается как имеющий тип noreturn, а код после цикла — недостижим. Такой цикл бесконечен локально. Однако при этом из цикла всё еще могут быть выходы через exit в более верхний синтаксический блок, или выход через return, или выход через rescue. Чтобы цикл рассматривался как бесконечный в абсолютном смысле, из него не должно быть синтаксически видимых путей выхода вообще.

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

В частности, таковыми признаются циклы, чьё тело состоит только из операторов pass и/или устранённых веток исполнения кода и/или вложенных вырожденных бесконечных циклов. (Но не ограничиваясь этими случаями - далее на усмотрение реализации и её возможностей анализа наличия побочных эффектов.)

Вырожденный бесконечный цикл реализация вправе заменить на вызов или инлайн функции qod.diverge. (От понятия divergent computation в теории вычислений.)

qod.diverge никогда не возвращает управление. На практике qod.diverge может быть реализована как инструкция процессора HLT, WFI или аналогичная, выполняемая в бесконечном цикле; либо иным образом, определяемым реализацией.

Конечные циклы

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

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

В случае, если такая трактовка нежелательна, следует использовать attribute(maybe_infinite).

wandrien ★★★★
()
Последнее исправление: wandrien (всего исправлений: 2)

Как всегда, в C++ что-то починили, по сути ничего не починив.

👍

dimgel ★★★★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария