Как стать автором
Обновить

Комментарии 206

Именно потому я и хочу Rust — они большую часть обещаний и ожиданий вынесли на уровень надзора компилятора, то есть компилятор будет не «молча проглатывать», а ругаться.
Просто включите в своем любимом компиляторе C/C++ все предупреждения.
Может помочь, а может и не помочь. Скажем пресловутое превращение конечного цикла в бесконечный никаких воплей с -Og не вызывает. А с -O2/-O3 вызывает, но если вы не компилируете с -Werror и отлаживаетесь с -Og, то вы можете этого и не заметить.

Это вообще большая проблема. Есть, конечно, ubsan, но вообще IMHO было бы гораздо лучше, если бы неопределённых поведений в стандарте было бы поменьше, а поведений, определяемых реализацией — побольше. Но, увы, стандарты пишем не мы.
И это по-прежнему позволит мне с лёгкостью вылететь за пределы массива, перепутать число аргументов у функции с va_list, попросить сделать memcpy для невалидного указателя, ошибиться с размерностью массива на всеми любимую единичку, забыть записать нолик в конце строки, передать указатель после free в функцию, сделать два раза free для одного и того же указателя…

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

Но… Может, хватит?
Ну а в чём проблема? Языков программирования уже больше, чем языков человеческих. Казалось бы, бери любой по вкусу.
Проблема в том, что до определённого момента не было языка одновременно гуманного к программисту и к компьютеру. Те языки, которые были гуманными, требовали, как минимум, GC. А это автоматом делало их негуманными к компьютеру. И вот появился rust, который пытается решить проблему гуманизации без GC. Если у них получится — это будет самый большой прорыв с момента появления Си.
НЛО прилетело и опубликовало эту надпись здесь
Вы говорите об одном и том же, собственно. Перечитайте что amarao написал ещё раз: во всех современных языках, которые постулируют «заботу о программисте» первое что разработчики делают — вкручивают туда GC. Будь то Java, Go, или C#. Что автоматически делает язык «ужасом летящим на крыльях ночи» с точки зрения железа.

Rust — редкое исключение. Посмотрим как у них получится.
Добро пожаловать в мир Objective-C ARC и Swift. Никаких GC и при этом очень дружелюбны.
Про Swift ничего не знаю (кроме того, что для 99% разработчиков это не вариант), а Objective C — это надстройка над C. Со всеми вытекающими.
Там действительно нет GC. Давно читал и нашел, почему-то, с трудом. Вот, посмотрите, хорошая статья, хоть и большая. Про сборщик мусора, начиная с «ВСЯ ПРАВДА О СБОРЩИКЕ МУСОРА» — habrahabr.ru/post/188580/
Какая разница — есть там сборщик мусора или нет, если вся конструкция на бо́льшей части систем не работает? На на Windows, ни на Android'е, ни на Linux'е…
Вы серьезно? Вы хотите добавить в Си (!!!) кучу оберток для всех перечисленных случаев только из-за того, что Вы не способны нормально программировать на этом языке? А получающийся при этом оверхэд вообще не смущает?
Как раз речь идёт о том, чтобы добавлять их не в рантайм, а на этапе компиляции. Если бы проблема с си была только во мне, то проблемы бы не было — но ошибаются нелепым образом мэтры программирования, и это бьёт по всем.
Для этого есть статические и динамические анализаторы. В чем проблема их использовать?
Какие есть динамические анализаторы, которые без поддержки компилятора гарантировано смогут обнаружить выход за пределы массива? BoundsChecker не предлагать, он вот такое не сможет обнаружить:
int main() {
    int a[4];
    int b[4];
    a[5] = 'a';
    return 0;
}

Динамические анализаторы находят разрушение памяти в зонах, не предназначенных для записи, а если записано просто в чужой массив, или память прочитана (см. HeartBleed), выявить это нельзя без поддержки компилятора.
Без поддержки компилятора никто не скажет, но с чего вы вдруг решили, что гланды нужно вырезать только и исключительно через задний проход? Если использовать самый простейший, встроенный в GCC анализатор, то он вам продробно всё раскажет:
$ cat test.c
int main() {
    int a[4];
    int b[4];
    a[5] = 'a';
    return 0;
}

$ gcc -g -fsanitize=address test.c -otest
$ ./test
=================================================================
==7769==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffefbce234 at pc 0x400803 bp 0x7fffefbce1f0 sp 0x7fffefbce1e8
WRITE of size 4 at 0x7fffefbce234 thread T0
    #0 0x400802 in main /tmp/5/test.c:4
    #1 0x7fcf7202576c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
    #2 0x400668 (/tmp/5/test+0x400668)

Address 0x7fffefbce234 is located in stack of thread T0 at offset 52 in frame
    #0 0x400767 in main /tmp/5/test.c:1

  This frame has 1 object(s):
    [32, 48) 'b' <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/5/test.c:4 main
Shadow bytes around the buggy address:
  0x10007df71bf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007df71c40: f1 f1 f1 f1 00 00[f4]f4 f3 f3 f3 f3 00 00 00 00
  0x10007df71c50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007df71c90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Contiguous container OOB:fc
  ASan internal:           fe
==7769==ABORTING
Это я случайно попал в red zone.
А если так:
int main() {
    int a[0x200];
    int b[0x200];
    a[0x300] = 'a';
    return 0;
}
А так — это ещё умудриться написать надо. Серебрянной пули нет, но если ваша задача — не доказать, что вы можете обмануть компилятор, а поймать ошибки, то AddressSanitizer работает вполне удовлетворительно. В конце-концов вы ведь просто можете вместо a использовать b — и это вам уже ни один компилятор ни в одном языке не отловит (хотя иногда может отловить статический анализатор). Количество ошибок «проскакивающих» мимо ASAN'а сравнимо с количеством подобных опечаток (и ещё неизвестно — чего больше).
HeartBleed был именно такой, что AddressSanitizer был бессилен ))
Речь о том, что в java мы бы моментально словили IndexOutOfRangeException.
HeartBleed был именно такой, что AddressSanitizer был бессилен ))
Srsly? Christopher T. Celi (of NIST) confirmed to me on 2014-07-10 that address sanitizer does detect Heartbleed if an attacking query is made against a vulnerable OpenSSL implementation — вам перевести? Выделение там в оригинале, если что.

Речь о том, что был бы Си Джавой, мы бы моментально словили IndexOutOfRangeException.
Конечно. Но это не спасло бы нас от кучи других проблем всё равно. А практически AddressSanitizer достигает именно этого в Си.
В случае с ограничениями Си, чаще всего GCC -Wall -Wextra показывает достаточно много чего полезного, и нормальные исходники почти не генерируют предупреждений. Но вот в случае поддержки тотально устаревших архитектур 20-30ти летней давности — включаем -Wtraditional и просто «рука-лицо». Если брать во внимание ограничения С++, то тут ситуация страшнее — включаем -Weffc++ и нет предела радости.
Я стараюсь каждый (под)проект, над которым доводится работать, вычистить до некоего максимума (когда исправления дальнейших варнингов уже чревато ошибками: обычно это связано с alignment и прочими проблемами двоичной совместимости), а в Makefile установить текущий «уровень чистоты» и -Werror, чтобы предотвратить появления новых варнингов (цитирую свой старый комментарий, там же пример кода для GNU make).
Когда я включаю в своем основном компиляторе все предупреждения, он никак не реагирует на:
"abcde"[0] = 5;
например.

Про strict aliasing я вообще молчу.

Ну не все же компиляторы одинаково хороши в этом плане. GCC и Clang с этим нормально справляются.
Вывод из статьи: программисты, которые пишут кроссплатформенные вещи на C, должны быть очень круты (почти также круты, как Чак-Норрис). А раз в реальности программы пишут обычные люди — имеем то, что имеем: куча багов и дыр вызвана такими неопределенностями и тем, что компиляторы не помогают их вовремя найти, потому что стандарт написан весь на компромиссах.
Мне лично в этом плане больше нравится подход компилятора go: под каждую архитектуру своя реализация платформо-зависимого кода, без каких либо извратов портабельности.
НЛО прилетело и опубликовало эту надпись здесь
Тут под кроссплатформом подразумевались особенности целевой архитектуры процессора, а не ОС.
Для пользователей которые дальше glibc не плавают, эти все вещи выглядят довольно эфемерно.

Учитывая особенности организации различных операций, допустим различных SIMD/MIMD в x86 Cortex-m3 ARMv7 ARMv9, можно добиться очень больших приростов производительности используя конкретные решения целевой архитектуры. Поэтому разработчикам кроссплатформенных решений проще абстрагироваться под особенности каждого процессора индивидуально. Компиляторы С/C++ чаще всего либо игнорируют все эти возможности, либо компилируют под что-то одно, игнорируя возможные оптимальные решения для конкретной задачи, но работает естественно быстрее чем пустой сишный лапшекод.

Вообще знатоки разнообразных ASM'ов довольно негативно выражаются в сторону C/C++ компиляторов — слишком много лапши: там где можно 1 специфическую инструкцию — пилят десять общих, ведь так безопасней и слоупочней).

Последние версии GCC вообще много чего могут поломать при -O3.

P.S. у разных архитектур свои ASM'ы, с разными наборами команд, и возможностями.
там где можно 1 специфическую инструкцию — пилят десять общих, ведь так безопасней и слоупочней).

Это зависит от конкретного процессора.
Бывает так, что десяток простых работает быстрее, чем 1 специализированная. Как, например, с LODSB — MOV/INC на некоторых моделях x86.
Это уже следующий этап: компиляторы не умеют генерировать условную LODSB -> условная LODSB в программах встречается редко -> при разработке следующих моделей процессора оптимизируют те инструкции, которые встречаются чаще -> условная LODSB становится медленнее пары MOV/INC.
Есть и ещё более следующий этап: поняв, что какой-нибудь MOVSB «в железе» может быть быстрее его таки делают весьма быстрым и заводят флаг в CPUID, который об этом говорит. После чего все встают на уши и разрабатывают целую специальную технологию позволяющую выбрать на лету один из двух вариантов.
Вы что-то совсем компиляторы загнобили. В общем и целом, компиляторы сейчас генерируют более качественный и оптимальный код, по сравнению с тем, что пишут люди. Когда вам нужно воткнуть единственную специфичную инструкцию, тут уже имеет смысл сделать asm вставку. На то язык и низкоуровневый.

Посмотрите хотя бы вот эту статью: llvm.org/docs/Vectorizers.html
Ерунда какая. Я, по работе, часто смотрю на код, который сгенерировали компиляторы. И глядя на этот код зачастую хочется плакать. Ну за исключением тех случаев, когда срабатывают уже готовые, подготовленные людьми peepholeы — тогда иногда действительно хочется «снять шляпу».

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

Собственно причина создания FORTRANа, Javaы, PHP и всех других языков одна и та же: за счёт отказа от написания непереносимого кода в машинных кодах увеличить производительность программиста. Си тут ничем не отличается. За исключением того, что он находится на самой нижней ступени иерархии: в нём есть ровно столько фич, чтобы было возможно писать код, который сможет работать на разных машинах — и не более того. Но этого уже довольно много, это уже замедляет результат весьма заметно. Несмотря на все потуги оптимизирующих компиляторов.

Да что там долго говорить: попробуйте написать проверку бита в битовом массиве так, чтобы она в одну команду «bt» на x86, а я на вас посмотрю. В любом компиляторе. Никак. Либо интринзик, либо ассемблерная вставка. А ведь эта операция в некоторых алгоритмах встречается ой как часто! Или сложение с диагностикой переполнения. Это очень экзотическая и никому не нужная операция? Srsly?
Как минимум Clang умеет.
Это не то, чего я просил. Я просил битовый массив. Не что-то, влазящее в int или регистр, а массив. Ну, скажем, на миллион битов. Извиняюсь за неточность.

Инструкция bt может с таким работать, но написать что-нибудь так, чтобы это оттранслировалось в одну инструкцию bt (плюс проверку или setXX) хотя бы на каком-нибудь компиляторе я не умею.
Чтобы было понятнее: я хочу избавиться от ассемблера вот в такой вот структуре. Или, на худой конец, объединить bitmap_bit_is_set и bitmap_is_bit_set. Пока ни того, ни другого мне не удалось придумать как сделать.
Если бы три года назад, написав свой комментарий, вы потратили ещё полчаса, реализовали такой peephole для GCC или LLVM, и запостили в мейлинг-лист — то к настоящему времени уже могли бы убрать из своего кода ассемблерные вставки ;-)
Поверьте, полчаса на это не то что мало, а смехотворно мало. Нужно будет написать peephole, написать тестов, обьяснить что это, зачем. Хорошо если за неделю переписки удастся это сделать.

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

Т. е. писать мультиплатформенные программы на си уже не актульно? Почему?
НЛО прилетело и опубликовало эту надпись здесь
Ядра переносимых операционных систем. Linux, FreeRTOS например.
НЛО прилетело и опубликовало эту надпись здесь
Может, если напрячься, такую можно придумать, но всё равно предпочту Qt какой-нибудь.
А с каких пор у нас Qt языком стал? То есть Qt как библиотека для совместимости с разными системами — Ok, годится. Но вы на чём писать-то будете? На Javascript'е?
НЛО прилетело и опубликовало эту надпись здесь
Базовые принципы в Си и в C++ одинаковы. Слегка отличается список неопределённых поведений, но и только. Так что почти всё, о чём говорится в статье относится в полной мере и к C++ и к ObjectiveC.
Ну, по ObjC вообще спецификаций нет, ни на уровне грамматики, ни на уровне ABI…
НЛО прилетело и опубликовало эту надпись здесь
Числа вы тоже складываете итераторами? Или вас от переполнений на C++ ангел-хранитель ограждает?
НЛО прилетело и опубликовало эту надпись здесь
В смысле: вы уже настолько воспарили в облаках, что у вас в программе целые числа не складываются или вас не волнует — будет ли работать программа, в которой они складываются?
НЛО прилетело и опубликовало эту надпись здесь
Насколько я понял автора, его точка зрения в том, что компилятор не придерживается правил, принятых в target микроархитектуре, а только правил, заданных C99 (или любым другим) стандартом.

Потому что компилятор си писали злые сишники, которые свои правила оптимизации затачивают на стандарт, а не здравый смысл архитектуры.

Простой примёр: раньше я не задумываясь проверял переполнение int как уход в минуса:
if ((counter += STEP) < 0) counter -= STEP; /* вернём обратно */
Теперь я понял, что злой гений компиляторописателя может меня просто разыграть: если я в counter не писал отрицательного числа, и STEP положительный, то компилятор имеет право вырезать этот if, как недостижимый. И пофиг ему, что такой трюк разрешён и эффективен на уровне микроархитектуры.
Это не моя точка зрения — это то, как разрабатываются компиляторы. Как я уже говорил: когда вы с выпученными глазами прибежите, метая громы и молнии, то услышите примерно следующее:
У вас программа не работает? Из-за того, что содержит UB? О, какой ужас. Ну, мы надеемся, вы её почините. Как хорошо, что это не наша проблема. RESOLVED INVALID
Самая же фишка в том, что он вовсе не всегда эти проверки будет вырезать. Он сможет этот if вырезать только в том случае когда выяснит, что counter положителен. А сделать он может это, например, выяснив что стартуете вы с нуля и всюду прибавляете только положительные числа. Я не зря приводил пример в статье — он как раз из той же оперы и очень-очень показателен.
Проблема в том, что если вы проигнорируете возможность возникновения переполнения, то можете быть жестоко «биты» даже там, где просто-напросто складываются два числа и сравниваются с третьим. Вам нужно будет написать всё так, чтобы нигде и ни в какой момент переполнение во время работы программы не возникало.

Даже если вы программируете на C++ и пользуетесь итераторами. Для кого-то это может быть и «базовые знания», а для кого-то тот факт, что превратить вашу программу в тыкву оказывается неожиданностью. Пример, который я приводил точно так же замечательно разнесёт в клочки вашу программу на C++, как он это делает на C.
НЛО прилетело и опубликовало эту надпись здесь
size_t это как бы C++, STL, вот это вот все.
А что, вы факториал не считали?
Бывает, бывает. Кроме size_t бывает и ssize_t, и ptrdiff_t и много чего ещё бывает в нашем мире.

К тому же переполнение при сложении чисел — это один-единственный UB. А ах десятки. И «контроль данных» нужно очень аккуратно делать, как уже писали, а то его компилятор просто вырежет «за ненадобностью».

Если вы никогда не наступаете на UB и никогда не применяете грязных трюков (типа if (this == NULL) — это как раз C++-специфические грабли), то я рад за вас. К сожалению не все знают что это может кончиться слезьми.
(this == NULL)
Ба-тю-шки, это как такое можно изобрести? А зачем оно?
Ну как бы некоторые считают, что так типа красивше. И на некоторых компиляторах даже работает. Но, к примеру, clang с необычайной решительностью этот код вырезает, так что вся «красота» летит к чертям.
Наверное это те граждане, которые в private — секции переменные портят, залезая в нее по смещению снаружи класса.

Ой, у них там еще delete this есть.
Ой, у них там еще delete this есть.

Я думаю, многие, кто пытался изобрести свой boost::intrusive_ptr, так или иначе приходили к
        void dec() throw() {
                long r = InterlockedDecrement(&refs);
                if (r == 0) delete this;
        }
Ба-тю-шки, это как такое можно изобрести?

Наверняка любой си-программист изобретал для себя эту мега-фичу.
Проблема в том, что дохрена книжек типа «си за 21 день» учит писать классы и пользоваться printf, но ни слова не говорит об UB. В учебных материалах эта тема старательно обходится.
«Писать классы и пользоваться printf» — очень тонкое замечание кстати. Кто ж будет cout использовать, в C++ — то.
НЛО прилетело и опубликовало эту надпись здесь
Я бы согласился, что пример «немного искусственный», если бы он не был в библиотеке, про которую, на минуточку, даже статья в Wikipedia есть.
НЛО прилетело и опубликовало эту надпись здесь
delete this законен при соблюдении двух правил:

1) Вы должны гарантировать что объект размещён с помощью «new» (то есть не в стеке)
2) Ни один метод после этой точки не использует сам объект.

То есть это «игра на грани фола», но, само по себе, ещё не катастрофа.
НЛО прилетело и опубликовало эту надпись здесь
Подскажите, пожалуйста, по примеру кода, что вы привели.
До этого вы всё говорили про UB, т.е. я предполагаю, что код на ту же тему, верно?
Я верно понимаю, что UB в строке?
int *q = (int*)realloc(p, sizeof(int));

Не могли бы пояснить, где именно?
В смысле я читал следующий за кодом абзац, но не понял про 3 переменные :(
Стандарт об этом ничего не говорит
p указывает на объект типа int, удаленный realloc'ом. Далее p используется.
Стандарт именно-что об этом говорит. Он явным образом запрещает использовать значение p после вызова reallocа. Простейший способ это использовать для оптимизаций — это «раздвоить» переменные. То есть, с точки зрения компилятора, переменная p₁ «умрёт» в месте вызова realloc'а и после него «родится» новая, никак со старой переменной не связанная, переменная p₂. Соответственно первую переменную можно будет положить на регистр, который «умрёт» во время вызова reallocа, а вторая — будет жить уже в другом регистре (а пусть даже и в том же!), но с первой никак не будет связана.
В этой строке нет UB. Оно в следующей строке. Там где используется p.

Из стандарта C11:
  All undefined behavior shall be limited to bounded undefined behavior, except for the following which are permitted to result in critical undefined behavior:
    The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.22.3).

Любая попытка использовать p после вызова reallocа даёт право компилятору отформатировать винчестер и запустить ядерную войну.
Конкретно в случае с realloc это не так — если realloc не справилась с выделением большего блока памяти, она вернет 0, а p не изменится и не высвободится.

То есть этому коду не хватает:
1. return(0); в конце.
2. Двух проверок успешного возврата из вызова библиотечных функций.
3. Высвобождения памяти после ее использования.
4. Констант. Хорошо было бы написать int * const p= [...], так как мы же не собираемся использовать p под что-то еще?
5. Понимания того, что malloc у нас был один, а указателей вдруг как-то стало два.
1. return(0); в конце не нужен.
  5.1.2.2.3 Program termination: reaching the } that terminates the main function returns a value of 0
2. Строго говоря да, но если вам повезло, то программа должна отработать.
  Отсутствие проверки на NULL — не UB. Вот обращение через NULL — это UB.
3. А это-то тут каким боком?
  Такого требования вообще ни в одном стандарте нету. Обычно система освобождает все ресурсы после выхода из программы.
4. Опять-таки — мы же не обсуждаем тут какой-нибудь Style Guide. Стандарт по этому поводу ничего не говорит.
5. А вот это — как раз то, в чём проблема.
По пункту 1 — В процессе жизни программы main переименуют во что-то и она перестанет ей быть, return в ней не появится, получите перекос стека, распишитесь.

2. Надежда у программиста на везение может привести к человеческим жертвам. А может не привести. Как повезет.

3. А, ну программа закончится, и ОС сама все освободит. А потом эту main куда-нибудь скопируют, и перестанет она быть main, а будет вертеться в цикле до скончания ОЗУ и подкачки.

4. const — это контракт с компилятором. Когда вы только соберетесь нарушить данное себе же обещание и испортить переменную, компилятор будет наготове.

Околосистемное программирование не позволяет оставлять что-либо без внимания.
1. Мы тут Си обсуждаем или ваши фантазии о нём?
  6.9.1 Function definitions
    If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

Где вы тут видите «перекос стека»? Или вы «не заметили» вторую часть довольно-таки коротенького предложения? Если функция ничего не возвращает — то это нормально. Конечно только до тех пор, пока кто-то не пытается использовать возвращаемое ей значение. Для main — это тоже нормально, так что непонятно — чем вы недовольны.

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

Всё возможно в этом мире. Смотреть нужно. Но отсутствие возврата из функции — это не UB и, соответственно, не ошибка в языке Си.

Корни у этого явления растут в истории Си: изначально в языке и типа void-то не было! Все функции «возвращали» int если не указывалось ничего другого. Но функции, которые ничего не возвращали — были (какая-нибудь free). И поскольку каждый байт был на счету там не было returnов. Потому этот вариант пришлось объявить законным. И законным он является до сих пор — даже в C11.

Но некоторые компиляторы, да, могут такого не любить. Это противоречит стандарту, так что должно быть описано в документации на компилятор, впрочем.
Это UB в C++ (6.6.3 [stmt.return] «Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.»), где возвращаемое значение может иметь разные интересные штуки вроде деструктора, оператора присваивания и быть подвержено RVO, но не в Си.
на сколько я помню, это не относиться к функции main()
Да, для main специальное исключение в 3.6.1 [basic.start.main] «If control reaches the end of main without encountering a return statement, the effect is that of executing return 0;» и в C++ тоже.
НЛО прилетело и опубликовало эту надпись здесь
В AX что-то, несомненно, попадало, но так как явного оператора return не было, то попадало бог знает что. Мусор какой-нибудь. Пока его не читали — на поведение программы это не влияло. И именно такое поведение кодифицировано во всех стандартах Си — вплоть до C11. В языке C++ ситуация иная, это правда.
Моего знания английского не хватает, чтобы это перевести однозначно, но мне кажется, что фраза
The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.22.3)

переводится как фраза
Значение по указателю, который указывает на участок памяти деаллоцированный с помощью вызова free или использования функции realloc

Т.е. если realloc не перенес блок памяти в другое место, т.е. он был не деаллоцирован, тогда мы с полной уверенностью можем заявить, что значение по указателю не изменилось, ввиду:
The content of the memory block is preserved up to the lesser of the new and old sizes, even if the block is moved to a new location

и выходит что это просто баг clang?
Еще больший нонсенс будет, если printf повторить:
>./a.out
1 2
2 2
Блок памяти может не быть деаллоцирован как уже было замечено только если не хватило памяти, а тогда realloc должен вернуть NULL.

То есть программа вызывает UB во всех случаях, но да, это могут быть разные UB:
1. Если realloc вернёт NULL, то будет обращение к нулевому указателю — но компилятор имеет право считать что этого никогда не произойдёт из-за строчки *p = 1.
2. Если realloc не вернёт NULL, то будет обращение к деаллоцированному объекту (ну а дальше — как описано в статье).
Блок памяти может не быть деаллоцирован как уже было замечено только если не хватило памяти, а тогда realloc должен вернуть NULL

я не понял этой фразы, или вы не поняли меня. Если адрес начала блока не перемещается, то я считаю, что старый блок не был деаллоцирован, и, согласно стандарту, значения в этом блоке памяти сохраняются в любом случае. В смысле мне кажется таким перевод стандарта.
2. Если realloc не вернёт NULL, то будет обращение к деаллоцированному объекту (ну а дальше — как описано в статье).

Да, давайте не будем вдаваться в подробности красивого кода, мы смотрим только на этот случай, потому что вряд ли у вас не может быть выделено sizeof(int). Почему здесь будет обращение к деаллоцированному объекту, если блок памяти не был перемещен?
Если адрес начала блока не перемещается, то я считаю, что старый блок не был деаллоцирован.
Стандарт говорит, что возможны ровно два варианта:
1. Блок памяти был деаллоцирован, данные были помещены в новый блок памяти, функция вернула не NULL.
2. Блок памяти не был деаллоцирован, данные не были перемещены, функция вернула NULL.
Всё. Больше вариантов нет.

В смысле мне кажется таким перевод стандарта.
Как вы можете по этому переводу что-то решать? Это просто описание одного из десятков UB. Как ведёт себя realloc описано в другом месте стандарта:
7.22.3.5 The realloc function
  The realloc function deallocates the old object pointed to by ptr and returns a pointer to a new object that has the size specified by size. The contents of the new object shall be the same as that of the old object prior to deallocation, up to the lesser of the new and old sizes. Any bytes in the new object beyond the size of the old object have indeterminate values.
  If memory for the new object cannot be allocated, the old object is not deallocated and its value is unchanged.
  The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object could not be allocated.

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

Почему здесь будет обращение к деаллоцированному объекту, если блок памяти не был перемещен?
Потому что старый объект может быть не деаллоцированным только в случае нехватки памяти и в этом случае realloc возвращает NULL. Если она вернула не NULL (а мы знаем, что она вернула не NULL из-за строчки *q = 2), то это — указатель на другой, новый, свежеаллоцированный объект. Хотя, как было явно замечено, it may have the same value as a pointer to the old object.

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

Конечно в данном, «игрушечном», примере UB возникает не просто при вызове определённой функции, а и при вызове всей программы целиком — но странно было бы «затачивать» компилятор только вот на такие игрушечные примеры.
Спасибо, за развернутый ответ!
The realloc() function tries to change the size of the allocation pointed to by ptr
to size, and returns ptr. If there is not enough room to enlarge the memory alloca-
tion pointed to by ptr, realloc() creates a new allocation, copies as much of the old
data pointed to by ptr as will fit to the new allocation, frees the old allocation,
and returns a pointer to the allocated memory.


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

в случае приведенного вами кода указатель q == p потому что realloc по сути сделал nop и не выделил новой памяти

вот вывод «1 2» это вопрос скорей именно к компилятору clang, на маке clang именно так и выдал, gcc на линуксовых машинах выдали «2 2»
The realloc() function tries to change the size of the allocation pointed to by ptr to size, and returns ptr.
А из какого стандарта вы это вытащили? Никаких-таких «tries to change the size» я ни в одном из них не видел, однако.

вот вывод «1 2» это вопрос скорей именно к компилятору clang,
С чего вдруг? Программа вызвала UB и имела право сделать что угодно. Программист должен это исправить и всё. Есть много способов это сделать: например можно перенести проверку if (p == q) { наверх и делать *p = 1; *q = 2; только после этого. Или вообще использовать только q. Но программа в том виде как она есть — гарантированно вызывает UB и, соответственно, должна быть исправлена.
А из какого стандарта вы это вытащили?

mac 10.9.5: man 3 realloc

linux.die.net/man/3/realloc
The realloc() function returns a pointer to the newly allocated memory, which is
suitably aligned for any kind of variable and may be different from ptr


гарантированно вызывает UB

не на всех компиляторах
# rm -f a.out; clang -O a.c && ./a.out
1 2
# rm -f a.out; gcc a.c && ./a.out
2 2


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

не на всех компиляторах
UB происходит на всех компиляторах. Это определяется только и исключительно стандартом языка и ходом исполнения программы (попадёте вы на ветку с вызовом UB или нет). Вот что конкретно программа выдаст — да, это зависит от компилятора. От его версии, опций компиляции и прочего. Если ваша программа «не взорвалась» от UB, то это не ваше счастье, а ваша беда: значит она взорвётся завтра. И не у вас, а у вашего заказчика. Вам от этого легче будет?
man является руководством к написанию рабочего (и, желательно, переносимого) кода и для разработчика не компиляторов покрывает практически (если не полностью) всю теорию описанную в томиках стандартов в части практического использования.

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

Во-вторых он не выверяется так тщательно, как тексты стандартов и потому может содержать ошибки. Почему и не может покрывать полностью «всю теорию описанную в томиках стандартов».

Практически — да, это полезное неформальное описание, но не более того.
вы хотите сказать что man realloc(3) для каждой системы описывает системо-зависимые реализации realloc?

на самом деле наша беседа уже вышла за рамки конструктивной.

то что вы предвержены стандартам это хорошо, но в перспективе ибо стандарт != реалии, а надо чтобы работало сейчас, а не когда группы стандартов договорятся между собой и перестанут друг другу гадить в багзиле.
вы хотите сказать что man realloc(3) для каждой системы описывает системо-зависимые реализации realloc?
man realloc(3) описывает функцию стандартной библиотеки с названием realloc. Стандарт языка описывает поведение языка в части работы с определёнными функциями.

Ну вот так получилось что работа с некоторыми «сильно специфическими» (и очень низкоуровневыми!) функции особо оговаривается в стандарте языка. Вот эти всякие memset, memcpy, calloc/malloc/realloc/free — они обрабатываются особо. Фактически это не совсем функции — скорее это некоторые конструкции языка, которые по историческим причинам также являются функциями.

Потому то, что написано в manе — это, конечно, хорошо, но недостаточно.
Здесь как раз очевидно, что realloc может поменять адрес блока данных, поэтому p — невалидно и использовать его программист не должен.
Если использует, то после ряда оптимизаций код может измениться настолько, что это приведёт к странному поведению.
Для iAPX 432 не было С компилятора, Ada / Smalltalk.
Сомневаюсь что именно для этой платформы можно было бы написать Си-компилятор.
Си-компилятор можно написать почти что для чего угодно. Вплоть до систем с троичной логикой. Главное — чтобы они были «достаточно большими» (в какой-нибудь ATiny C просто «не влезет»).

Хотя да, конечно, когда iAPX 432 появился C ещё не был так популярен и, скорее всего, компилятора такого никогда не было. Но это мало что меняет: не хотите iAPX 432, возьмите E2k, там подобные же ограничения.
Надо искать стандарт того времени.
Но, к примеру, shift-операторов в ISA 432 не было.
А логикой и арифметикой их очень дорого делать.
Возможно. Но как я сказал: это непринципиально. Стандарт ведь разрабатывается не для систем, на которые Си уже портирован, а для систем, на которые его можно портировать.

Не хотите iAPX 432 — возьмите какой-нибудь 80286й, на нём всё та же проблема и уж там-то Си точно был.
Си-компилятор можно написать почти что для чего угодно. Вплоть до систем с троичной логикой.

А это как, если в C явно определены бит и байт? Игнорировать третье состояние трита и эмулировать двоичное представление данных?
Где это в Си вы обнаружили типы бит и байт, я извиняюсь? Или вы про int8_t? Ну так он опционален. Обязательны int_least8_t и int_fast8_t, а типа int8_t может и не быть.

Сдвиги там определены как s E1 × 2E2 и E1 / 2E2 соответственно, что вполне можно реализовать и в системе с троичной логикой. То же касается всех остальных операций. Хотя эффективность будет, конечно, ни к чёрту, но портировать можно.
Где это в Си вы обнаружили типы бит и байт, я извиняюсь?

Вот где:

С11 3.5: bit — unit of data storage in the execution environment large enough to hold an object that may
have one of two values

С11 3.6: byte — addressable unit of data storage large enough to hold any member of the basic character
set of the execution environment (NOTE: A byte is composed of a contiguous sequence of bits)

Сдвиги там определены как E1 * 2^E2 и E1 / 2^E2 соответственно

Полное определение такое (C11 6.5.7.4):

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2^E2, reduced modulo one more than the maximum value representable in the result type.

По этому определению число E1 сдвигается влево на E2 битов, нижние биты заполняются нулями, и результат равен E1 × 2^E2. Из этого можно заключить, что сдвиг на одну позицию умножает число на 2, то есть, в представлении данных используется двоичная система счисления.
Ok, уели. Системы с троичной логикой, похоже не поддержать. Но заметьте, что на какой-нибудь БЭСМ-6 у вас Сишный «байт» может хоть 48-битным, хоть 6-битным в зависимости от фантазий разработчиков компилятора. То же касается некоторых моделей Cray и/или всяко-разных микроконтроллеров.
6-битным не получится, потому что CHAR_BIT должен быть не менее 8 :)
Сволочи! Они всех погубить хотят! Как теперь ностальгировать по славным временам, когда 6-буквенный идентификатор отлично вписывался в 36-битное машинное слово на PDP-10?

Хотя, может, оно и к лучшему…
Но, минуточку, если вы таки пойдёте по ссылке в Википедию, то обнаружите, что UNIX — она не только многозадачная и многопользовательская, но она ещё и переносимая

А вот что значит, что она переносимая? ABI универсальный? Или что у неё стандартизован интерфейс взаимодействия, типа POSIX?
Это значит что для того, чтобы запустить её на другой платформе нужно исправить небольшой процент её кода. Конечно тут сразу же возникает вопрос на тему «а какой процент считается большим», но, в общем, с тем, что «UNIX — переносимая система» никто особо не спорит. Этот вопрос скорее возникает при обсуждении всяких FIG-FORTHов.
Автор бы как-то подитожил: какие компиляторы «хорошие» а какие «плохие», где правильно обрабатываются неопределённые ситуации а где нет. Указали бы чем лучше пользоваться. А так статья кажется незавершенной — проблема есть, а решения нет…
Причём тут компилятор? И как вы будете определять его «плохость»? По количеству UB, которые в нём переквалифицированы в IB? А какая, собственно, разница?

Вся статья — как раз о том, что не бывает «плохих» и «хороших» компиляторов. Есть «плохие» программисты (которые пишут программы с UB) и «хорошие» программисты (которые пишут программы, которые не вызывают UB).

А с компиляторами всё просто: программы написанные «хорошими» программистами они, в большинстве своём, обрабатывают правильно, а что будет с программами, написанными «плохими» программистами их «не волнует». Причём, что характерно, это «не волнует» (причём не волнует от слова «совсем») никого из них.
У вас программа не работает? Из-за того, что содержит UB? О, какой ужас. Ну, мы надеемся, вы её почините. Как хорошо, что это не наша проблема.
Вот весь смысл — в последнем предложении.
Случаи «оптимизации» неопределенного поведения компиляторами, приведенные в статье, напоминают следующую аналогию.

Предположим, у нас есть высоковольтный щит с оборудованием, которое не изолировано в целях экономии. В этом щите, под напряжением, человек проводит ремонтные работы. Известно множество случаев поражения электротоком при работе в таких щитах. Но что поделаешь — экономия на изоляции себя оправдывает, и ремонтники должны повышать свою квалификацию, чтобы избежать поражения. Но вот изобретается аналог «оптимизирущего компилятора» — интеллектуальная система видеомониторинга, которая следит за движениями человека и позволяет обнаружить, что его рука вот-вот коснется провода под напряжением. И что делает эта система? Отключает напряжение? Дает сигнал тревоги? Нет, она повышает напряжение на проводе, чтобы удар был сильнее и опаснее.
Ну надо искать злой умысел там, где его нет. Никакого такого мониторинга у компилятора нету. Он просто-напросто повышает напряжение (чтобы всё побыстрее работало) и старается расположить провода поближе друг к другу (ну чтобы ремонтнику было бы удобнее работать, ага). Ну а дальше — да, конечно, это приводит к тому, что ремонтника начинает бить током чаще.

Просто положительный эффект от оптимизаций (уменьшение кода и ускорение) видят все и всегда, а вот отрицательные — только тогда, когда из-за них вас «ударило током». То есть когда у вас if (a + 10 > b + 10) превращается в if (a > b), то вы только радуетесь и говорите «у, какой умный компилятор, надо же», а когда, следуя той же логике, он превращает if (a + 10 < 0) (проверка на переполнение intа) в if (false) (с последующим выкидыванием «мёртвого» кода) то вы возмущаетесь и начинаете «метать громы и молнии».

Но у компилятора-то ни телепатии, ни здравого смысла нету — он «и понятия не имеет» чего вы реально хотите добиться, он просто выкидывает из программы куски, которые в ней, если программа правильная и не вызывает UB не нужны, только и всего!
Было бы все только на ворнингах, без изменения потока управления, было бы отлично. Но для этого есть статические анализаторы…
Не понял? Как вы можете сделать хоть какую-то оптимизацию «без изменения потока управления»?
Я больше про UB, который может «сломать» программу. Плохо, когда оптимизация делается втихую. Я бы предпочел видеть, что инструкции удалены или изменены из-за UB.
Это вам так кажется. Как только вы увидите, что простейшая программа, использующая STL, порождает буквально стони предупреждений в простейших конструкциях вам это делать перехочется. Конечно большая часть этих UB невозможно вызвать на практике (например они, теоретически, могут сработать если размер какого-нибудь контейнера превысит половину размера доступного адресного пространства — но на практике вам столько памяти аллоцировать вряд ли дадут), но анализ, который для этого нужно производить выходит очень далеко за рамки того, что может «знать» компилятор.

Или вы действительно хотите видеть предупреждение каждый раз, когда компилятор сокращает константы? Чегой-то я очень сильно в этом сомневаюсь.
Это вполне может быть настраиваемым. Уровни предупреждений.
STL отдельный разговор, могла бы быть исключением. Сотни предупреждений — результат принципа реализации стандартной бибилиотеки как набора шаблонов, макросов. Поскольку это _стандартная_ библиотека, вполне могла бы обрабатываться особо.

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

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

То есть вы хотите превратить UB в IB. Это, возможно, имеет смысл в каких-то случаях, но может очень сильно ударить по производительности. В частности превратить «a + 10 > b + 3» в «a + 7 > b» будет уже нельзя. Вы этого хотите?

STL отдельный разговор, могла бы быть исключением. Сотни предупреждений — результат принципа реализации стандартной бибилиотеки как набора шаблонов, макросов. Поскольку это _стандартная_ библиотека, вполне могла бы обрабатываться особо.
STL просто используется [почти] всеми программами. Другие C++ библиотеки тоже часто написаны в таком же стиле. Тот же Boost.
Да что там, все так пишут:
#ifdef TRACE_ENABLED
#define LOG printf
#else
#define LOG if (false) printf
#endif

Потом компиляция потонет в бесконечных сообщениях «блок if (false) printf(...); удалён из программы как недостижимый».
Вы этого хотите?

Нет; меня более беспокоят ситуации вида «раз указатель не проверяется на NULL до дереференса, считаем, что есть сбой». Хотя он, может быть, проверяется до вызова функции.

Другие C++ библиотеки тоже часто написаны в таком же стиле

Что поделать — макросы…
STL отдельный разговор, могла бы быть исключением. Сотни предупреждений — результат принципа реализации стандартной бибилиотеки как набора шаблонов, макросов. Поскольку это _стандартная_ библиотека, вполне могла бы обрабатываться особо.
Вот это нехорошо. Если я использую стандартную библиотеку, то у меня всё клёво, мусорных сообщений нет. Но стоит мне сделать шаг влево и самому написать нужную мне шаблонную магию в обход STL/boost — и я тут же получаю стену бессмысленных предупреждений.

И в стандарт потом включать специальный атрибут [[nowarn(unsigned::addition::overflow)]], чтобы размечать свой код, затыкая рот компилятору с его предупреждениями…

Это вполне может быть настраиваемым. Уровни предупреждений.
Компиляторы и так могут выдавать отладочную информацию о том, что они делают на каждом проходе. Обширную. Вперёд писать парсер для неё. Только это называется статическим анализатором. Для примера из поста, после SSE-преобразований очевидно, что первый аргумент realloc() используется после вызова, чего делать нельзя.

Потенциально нельзя. Но не абсолютно всегда нельзя. Если указатель не изменился, то можно. Но компилятор не знает, как там realloc() реализована.

Но ещё раз. Это статический анализатор, а не компилятор. Отдельная утилита. Компилятор тоже вполне может ругаться на это, но заставлять его делать — это отвлечение от его настоящей работы. Для каждого дела следует использовать наиболее подходящий инструмент. А то так можно дойти и до того, почему компилятор не генерирует по умолчанию UML-диаграммы для компилируемого кода, ведь «инструмент-то есть»!
Но компилятор не знает, как там realloc() реализована.
Не только компилятор не знает. Программист тоже не знает. Например можно вполне себе представить реализацию, которая вызов reallocа даже с тем же самым размером, который был аллоцирован изначально использует как повод для компактификации — тогда данные могут быть передвинуты даже в этом случае. Это — абсолютно законный вариант реализации reallocа. Вот если вы проверите равенство p и q — можете использовать любой из них. Тогда и компилятор перестанет чудить.

Это из той же оперы, что и
int a(int* const p)
{
    printf("%d",*p);
    // ... do something
    if(p==NULL)
        return 1;
    return 0;
}
Нельзя так писать. Просто нельзя.
С примером из статьи согласен. Использовать можно только q, результат реаллока.

Нельзя так писать. Просто нельзя

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

В оригинале как раз проблема заключалась в том, что проверки до разименования не было из-за чего случалась уязвимость.
Вот это нехорошо. Если я использую стандартную библиотеку, то у меня всё клёво, мусорных сообщений нет. Но стоит мне сделать шаг влево и самому написать нужную мне шаблонную магию в обход STL/boost — и я тут же получаю стену бессмысленных предупреждений.


Это проблема любого макроса. Не думаю, что есть хорошее общее решение.

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

Если он готов выкинуть блок-другой кода, и не потому что это повторяющиеся вычисления и прочие очевидные ситуации, как та, что привел khim с вычислением выражения, то это уже не его настоящая работа. Мне не нравится, когжа инструмент превращается в черный ящик (и, еще раз — я не говорю об очевидной оптимизации типа «мертвый код»).
Никакого такого мониторинга у компилятора нету.

Ну как же нету, если он применяет упомянутые оптимизации только тогда, когда заведомо установил наличие в программе UB?

Прямая аналогия. Система мониторинга видит, что человек вот-вот коснется оголенного провода (установлено наличие в программе UB). Но вместо вывода сообщения об ошибке происходит «оптимизация». Кому нужна такая оптимизация, если она приводит к заведомо нежелательному результату?

И не надо говорить, что я де-мол против оптимизаций. Нет, я не против. Но оптимизации бывают разные. Одни улучшают характеристики программы, не изменяя ее семантику. А другие — усугубляют последствия ошибок. Причем на этапе компиляции видно, какой случай имеет место. Это то же самое, как если отключить диагностику некоторых ошибок компилятором и генерировать в случае обнаружения таких ошибок код, исполняющий то, что вздумается левой ноге автора компилятора. И оправдание: «но программист же не должен делать ошибок, это он сам виноват».
Когда компилятор заведомо установил наличие гарантированного UB, он выводит предупреждение. Остальные случаи — это лишь возможное UB. Си — это язык для думающих людей, и один из его принципов — не мешать программисту делать то, что он хочет. Поэтому когда компилятор видит вероятное UB, он подразумевает, что программист так написал код, потому что ему надо, и он подумал о том, что UB никогда не должно возникнуть в данном случае.

Кроме того, есть и другие проблемы. Часто UB вскрывается только после нескольких проходов оптимизатора, когда компилятор уже понятия не имеет, что именно в исходной программе вызвает возможность получить неопределённое поведение. И тут уж приходится идти на компромисс: или серьёзно замедлять время компиляции, чтобы не терять эту информацию и выдавать предупреждения; или вставлять рантайм-проверки, уменьшая производительность программы; или выдавать 100500 ложных предупреждений «у вас в коде, возможно, что-то не так, но я даже не могу сказать, где»; или забить и предположить, что программист не болван и сам обо всём позаботился.
Когда компилятор заведомо установил наличие гарантированного UB, он выводит предупреждение.

Во-первых, он в таком случае должен выводить не предупреждение, а ошибку. Не вы ли адепт той точки зрения, что программист ни при каких условиях не должен допускать UB в программе? В таком случае отказ от компиляции правомерен, понуждая программиста следовать этому правилу. Наоборот, продолжение компиляции с «оптимизацией» (с выводом варнинга или без него) — это намеренное усугубление последствий ошибки, это создание затруднений для ее последующего обнаружения.

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

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

оно чем обосновано? Вы можете привести примеры таких оптимизаторов, которые не могут восстановить соответствие генерируемого и исходного кода? Например, в Visual Studio даже при включенной оптимизации сохраняется debug info, позволяющее почти всегда исполнять программу пошагово (хоть код уже и был оптимизирован). Значит, эта информация доходит до самых последних стадий компиляции, а значит, и до тех, где потенциально может быть обнаружено UB. Между тем, компилятор от Microsoft считается чуть ли не самым быстрым на рынке. Так что нельзя говорить и о «серьезном замедлении времени компиляции».

В том примере, который приведен в статье, была рассмотрена нетривиальная «оптимизация», когда компилятор обнаружил UB и в этой связи сгенерировал код, который делает нечто, не похожее даже приблизительно на исходный текст программы. Вы считаете, что компилятор прав: он воспользовался неопределенностью поведения, и в результате был абсолютно свободен в своем выборе, какой код генерировать. Но почему тогда оптимизация не пошла дальше? Почему она вообще не прекратила генерацию кода в месте, где было обнаружено UB и далее по потоку управления? Гораздо более оптимально, и по скорости, и по размеру программы, просто поставить в этом месте вызов abort(). Это ведь тоже вписывается в разрешенные последствия UB.
Во-первых, он в таком случае должен выводить не предупреждение, а ошибку.
Нет, нет и нет. 100500 раз нет. Ситуация когда компилятор может обнаружить такую ошибку, которая вызывает UB всегда, при любом запуске программы — в природе почти не встречаются. За исключением крошечных, игрушечных, специально для этого сделанных программ.

Гораздо чаще бывают ситуации когда отдельные функции или отдельные участки кода в функциях обязательно вызывают UB. Но если они никогда при работе не вызываются — то программа корректна и потому должна скомпилироваться и заработать. Компилятор, разумеется, может эти участки кода выкинуть за ненадобностью — они же никогда не должны вызываться!

В таком случае отказ от компиляции правомерен, понуждая программиста следовать этому правилу.
Это, собственно, неплохая идея, но её реальзация создаст совсем другой язык. Rust к примеру, пытается так делать. Подход же Си другой: в программе могут быть участки, которые потенциально вызывают UB (да, чёрт побери, без этого вы вообще нифига написать не сможете ибо у вас простейший цикл от «a» до «a + b» может теоретически вызвать UB...), она может даже на 99% состоять из них. Но если они реально во время работы программы не происходят (например потому что соответствующие функции не вызываются) — вы в шоколаде.

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

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

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

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

Так что нельзя говорить и о «серьезном замедлении времени компиляции».
Ах, вы про это. Вообще-то для большинства людей важна не скорость работы компилятора (всегда можно прикупить ещё пару стоек машин), а скорость работы скомпилированного кода. Если вас это не волнует — компилируйте с "-O0" и можете забыть про UB.

Гораздо более оптимально, и по скорости, и по размеру программы, просто поставить в этом месте вызов abort(). Это ведь тоже вписывается в разрешенные последствия UB.
Вы правы не на 100%, а на все 200%. Когда clang может доказать, что где-то обязательно вызывается UB — он так и делает. Только он вставляет не вызов abort, а инструкцию ud2 — так дешевле. Мог бы и вообще ничего не вставлять, но эксперименты показали, что в этом случае программисты за ножи хвататься начинают.

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

Компилятор не занимается поиском случаев в которых всегда происходит UB. Он исходит из того, что этим занимается программист (сам или с помощью соответствующих инструментов) и ему с этим уже заморачиваться не нужно. Написано "a + b > b + c"? Раз мы знаем, что UB не происходит — можем превратить в "a > c". Написано "*p = 1;"? Значит мы знаем, что p у нас в этой точке — не NULL — можем выкинуть лишние проверки. И т.д. и т.п. Иногда после всех этих оптимизаций у нас не остаётся ничего — ну тогда можно и пожаловаться вполголоса. А если что-то осталось — то вот это и будет нашей программой. И если программист «хороший» и «за свои слова отвечает» — то это будет даже правильной программой.
Ситуация когда компилятор может обнаружить такую ошибку, которая вызывает UB всегда, при любом запуске программы — в природе почти не встречаются.

В вашей статье описана как раз такая ситуация. Если бы компилятор не доказал, что UB происходит при любом запуске программы, он бы не провел описанную вами «оптимизацию».
Но если они никогда при работе не вызываются — то программа корректна и потому должна скомпилироваться и заработать. Компилятор, разумеется, может эти участки кода выкинуть за ненадобностью — они же никогда не должны вызываться!

Совершенно верно. Пусть выкидывает, заменяя их на abort() или инструкцию, генерирующую исключение, ud2 или какую там. Еще лучше — с выводом осмысленного сообщения об ошибке типа «Code with undefined behavior was reached». Есть же в С++ runtime errors типа «Pure virtual function call», вот пусть будет и это.

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

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

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

Кому понятно? Мне непонятно. Почему же? Только потому, что стандарт это позволяет? Так он позволяет и вообще варнинги при компиляции не печатать. И оптимизацию не проводить. И много еще чего. Тут вопрос в качестве компилятора, его стремлении помочь программисту, облегчить его работу. Или наоборот, навредить, затруднить. Что первично — компилятор для программиста или программист для компилятора?
На статьях, ссылки на которые были ровно под вашим сообщением.

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

А вы разработчик компиляторов? Хотите задавить авторитетом вместо того, чтобы приводить логические доводы?
Если вас это не волнует — компилируйте с "-O0" и можете забыть про UB.

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

Да, UB не всегда может быть обнаружено на стадии компиляции при самом сильном старании компилятора. Но хотя бы в тех случаях, когда оно обнаружено и доказано — следует диагностировать ошибку, а не наказывать программиста внесением в программу более тонких и труднообнаружимых ошибок там, где стандарт это вроде бы позволяет.
Когда clang может доказать, что где-то обязательно вызывается UB — он так и делает.

Почему он этого не сделал в приведенном вами примере с realloc? Он ведь доказал наличие UB, иначе не мог бы провести «оптимизацию». Примеры в комментариях показали отключение злонамеренной «оптимизации» путем включения realloc в обертку, чтобы компилятор не мог доказать UB.
А это — уже другой вопрос. Нужно разбираться. «Ну не шмогла я, не шмогла»

Тогда это уже вопрос качества компилятора. Недоработок в нем. Но никак не оправдание подхода: «наказывать оптимизацией за UB».
Компилятор не занимается поиском случаев в которых всегда происходит UB

В приведенном вами примере — занимается. И обнаруживает.
Написано "*p = 1;"? Значит мы знаем, что p у нас в этой точке — не NULL — можем выкинуть лишние проверки

Если компилятор видит, что проверки «лишние» — должен быть напечатан варнинг, проверки оставлены. Если программист обратит внимание на варнинг и поставит проверки до разыменовывания указателя — то проблема исчезнет. Если программист не обратит внимания на варнинг — то есть шансы получить хоть какую-то диагностику при работе программы, когда эти проверки сработают. Программа будет работать чуть медленнее, чем если бы проверок не было, однако, раз проверка есть в исходном коде — значит программист ее желал и был готов к соответствующему падению скорости исполнения. Выкидывание проверки в таком случае — явный отход от указаний программиста, внесение в код труднообнаружимых уязвимостей и неопределенного поведения в других частях программы из-за перепаханной памяти.
И если программист «хороший» и «за свои слова отвечает»

Программист — это человек. Человеку свойственно ошибаться. Система, с которой работает человек, может помогать ему избежать ошибок, а может усугублять последствия. Давайте тогда на атомных электростанциях введем алгоритмы, выводящие стержни управления реактором из активной зоны в случае, если оператор совершит какое-нибудь запрещенное инструкциями действие, хотя бы прямо и не приводящее к таким последствиям. А что? Оператор виноват, инструкцию нарушил. Должен был «отвечать за свои действия» и «повышать свою квалификацию».
Но в таких ситуациях компиляторы вроде clang как раз и не проводят «оптимизацию». «Оптимизация», искажающая семантику программы, которую вы так приветствуете, происходит только в случаях, когда наличие UB может быть установлено компилятором.
Да ну? Вы что — последователь Аристотеля и считаете, что можно узнать как работает мир глядя только на собственный пупок?

Уж проверили бы, что ли:
#include <stdio.h>

int test(int i, int j) {
  return (i + 10 > j + 10);
}

int main(){
  printf("%d\n", test(1073741820, 2147483640));
}

$ clang -O0 test.c -o test0
$ clang -O2 test.c -o test2
$ ./test0
1
$ ./test2
0
Получите и распишитесь.

Но раз уж компилятор взялся за тяжелую задачу обнаружения UB.
Где брался? Кто брался? Когда? Компилятор — не брался.

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

А вы разработчик компиляторов?
Сейчас — нет. Ушёл из команды, разрабатывавшей один порт GCC пару лет назад. Сейчас работаю в команде, которая пилит JIT, что, конечно, не совсем то же самое. А что? Это что-то меняет? Конкретно в LLVM/Clang'е я не засветился, если что.

Хотите задавить авторитетом вместо того, чтобы приводить логические доводы?
То есть когда некий Вася говорит, что он, Вася, не может сделать того-то и сего-то, то это уже называется «задавить авторитетом»? Кто, кроме Васи, может это сказать?

Когда в официальном блоге LLVM люди объясняют, что они не могут сделать хорошего детектора UB, то как я могу им не верить? Они разрабатывают Clang, не я. Вы не верите, что они это говорили или не можете найти? Или вы из тех, кому мало положить в рот и пожевать, нужно ещё ударить по голове пару раз бейсбольной битой, чтобы пища в глотку пролезла? В любом случае вот, пожалуйста:

Why can't you warn when optimizing based on undefined behavior?
People often ask why the compiler doesn't produce warnings when it is taking advantage of undefined behavior to do an optimization, since any such case might actually be a bug in the user code. The challenges with this approach are that it is 1) likely to generate far too many warnings to be useful — because these optimizations kick in all the time when there is no bug, 2) it is really tricky to generate these warnings only when people want them, and 3) we have no good way to express (to the user) how a series of optimizations combined to expose the opportunity being optimized.
Если английского не знаете — сходите в Google Translate или переводчика попросите знакомого. А то ведь обвините меня в том, что я вас обманываю и «давлю авторитетом».

Если компилятор видит, что проверки «лишние» — должен быть напечатан варнинг, проверки оставлены.
Попробуйте создать такой компилятор — я посмотрю кто сможет им пользоваться. Бессмысленных проверок в любой программе сколько-нибудь нетривиального объёма — чуть более, чем дофига.

Программа будет работать чуть медленнее, чем если бы проверок не было, однако, раз проверка есть в исходном коде — значит программист ее желал и был готов к соответствующему падению скорости исполнения.
Вы действительно в это верите? Не придуриватесь? Неспособен на это программист. Причём неспособен от слова «совсем». Когда у вас функции inlineаться, то у вас возникают просто тысячи ненужных проверок. Весь STL заточен под то, что компилятор их выкинет! И Boost! В библиотеках Си это чуть менее критично, но всё равно — отказ от выкидывания лишнего кода приводит к замедлению не на проценты, а в разы!

Хотите сделать свой SuperTormozCC — делайте. Потом попробуете кого-нибудь убедить им воспользоваться. Но ради бога, не нужно в чужой монастырь со своим уставом ходить и объяснять разработчикам Си как тот язык, который они разработали, должен быть устроен.
Уж проверили бы, что ли:

Получите и распишитесь.

И что доказывает этот пример? То, что компилятор обнаружил UB? Или наоборот, не обнаружил? Какой был сгенерирован код в обоих случаях?
Где брался? Кто брался? Когда? Компилятор — не брался

Если бы не брался — то не было бы и самого понятия «оптимизации, основанной на UB». Раз такая оптимизация есть, раз она применяется только в случаях обнаружения UB — значит компилятор обнаруживает UB, когда применяет такую оптимизацию.
Как можно использовать результаты, которых нет — науке неведомо.

Ну как же нет? Если бы не было результатов — не было бы и «оптимизации», основанной на них.
Вы не верите, что они это говорили или не можете найти?

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

Вы действительно в это верите? Не придуриватесь? Неспособен на это программист. Причём неспособен от слова «совсем».

Тем не менее программист, с вашей точки зрения, должен быть способен обеспечить отсутствие UB в программе. Но по-моему задача выкидывания ненужных проверок значительно легче, чем отслеживание всех случаев UB. Да и последствия неверного решения первой задачи значительно более мягкие (замедление работы), чем второй (полное нарушение семантики).
Когда у вас функции inlineаться, то у вас возникают просто тысячи ненужных проверок. Весь STL заточен под то, что компилятор их выкинет!

Проверки проверкам рознь. Одно дело, когда компилятор видит, что такая же проверка уже была сделана ранее. Например, в таком варианте:
int a(int* const p)
{
    if(p==NULL)
        return 1;
    // ... do something ...
    if(p==NULL)
        return 1;
    return 0;
}

В этом случае вторую проверку можно выкинуть. Но если у нас имеется проверка, после использования указателя:
int a(int* const p)
{
    printf("%d",*p);
    // ... do something
    if(p==NULL)
        return 1;
    return 0;
}

то предсказывать результаты этой проверки исходя из того, что указатель был разыменован — это UB-based optimization, и от таких оптимизаций только вред. И я не вижу принципиальных причин, почему компилятор не сможет отличить один случай от другого.
Но ради бога, не нужно в чужой монастырь со своим уставом ходить и объяснять разработчикам Си как тот язык, который они разработали, должен быть устроен.

А вы разработчик языка Си? Участвовали в разработке стандарта?
Вы что — последователь Аристотеля и считаете, что можно узнать как работает мир глядя только на собственный пупок?

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

Если английского не знаете — сходите в Google Translate

Подобный стиль аргументации, безусловно, придает вашим доводам большой вес!
И что доказывает этот пример?
Что вы «Фома Неверующий», очевидно.

То, что компилятор обнаружил UB?
То что компилятор проигнорировал UB. Как и должен был.

Или наоборот, не обнаружил? Какой был сгенерирован код в обоих случаях?
Проверить не судьба?

$ clang test.c -S -o-
...
        .type   test,@function
test:                                   # @test
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbp
.Ltmp0:
        .cfi_def_cfa_offset 16
.Ltmp1:
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
.Ltmp2:
        .cfi_def_cfa_register %rbp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    -4(%rbp), %esi
        addl    $10, %esi
        movl    -8(%rbp), %edi
        addl    $10, %edi
        cmpl    %edi, %esi
        setg    %al
        andb    $1, %al
        movzbl  %al, %eax
        popq    %rbp
        retq
...

$ clang -O2 test.c -S -o-
...
        .type   test,@function
test:                                   # @test
        .cfi_startproc
# BB#0:                                 # %entry
        cmpl    %esi, %edi
        setg    %al
        movzbl  %al, %eax
        retq
...

В результате оптимизации, как и следовало ожидать, "+ 10" из обоих сторон выражения пропали. Результат этот оптимизации для любой программы, которая не вызывает UB — валиден, для той которая вызывает — может быть «невалиден» (невалиден в кавычках, так как у программы, которая вызывает UB никакого «валидного» результате нет, а значит нет и «невалидного», но можно определить валидный как «результат работы программы, реализованной самым простым способом без всяких оптимизаций»).

Заметьте, что подобная же оптимизация для чисел без знака — невозможна, так как у них переполнение не вызывает UB, что тоже легко проверить:
$ cat test2.c
#include <stdio.h>

int test(unsigned i, unsigned j) {
  return (i + 10 > j + 10);
}

int main(){
  printf("%d\n", test(1073741820, 4294967290));
}

$ clang -O2 test2.c -S -o-
...
        .type   test,@function
test:                                   # @test
        .cfi_startproc
# BB#0:                                 # %entry
        addl    $10, %edi
        addl    $10, %esi
        cmpl    %edi, %esi
        sbbl    %eax, %eax
        andl    $1, %eax
        retq
$ clang -O2 test2.c -o test2
$ ./test2
1


Если бы не брался — то не было бы и самого понятия «оптимизации, основанной на UB». Раз такая оптимизация есть, раз она применяется только в случаях обнаружения UB — значит компилятор обнаруживает UB, когда применяет такую оптимизацию.
Ok. Покажите мне конкретно где в примере показанном выше компилятор «обнаружил UB».

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

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

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

Рассмотрим ваш пример. Сам факт того, что в нем присутствовало выражение:
a+c > b+c

которое, при отсутствии UB, эквивалентно менее затратному
a > b
, свидетельствует о некоторой небрежности программиста, отсутствии с его стороны длительных размышлений над этим местом программы. Не знаю, как другие программисты, но я обычно не полагаюсь на способности компилятора без необходимости и оптимизирую выражения сам. И если из-под моей клавиатуры появится выражение вида
a+c > b+c
— то исключительно по небрежности и недосмотру.

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

И еще о вашем примере. Выражение
a+c > b+c
эквивалентно
a > b
только при отсутствии переполнения, причем даже неважно, знаковые числа или нет. То, что компилятор не стал оптимизировать выражение для беззнаковых чисел — это следствие того, что в стандарте беззнаковое переполнение не является UB. Если бы являлось — можно было бы оптимизировать и тут. Стандарт в данной части имеет некоторую асимметрию, а совершенство обычно обладает симметрией, поэтому давайте подумаем, какой вариант симметрии был бы предпочтительнее?

1) можно объявить беззнаковое переполнение UB. Радость для компиляторов. Открывает новые возможности оптимизации.

2) можно определить поведение при знаковом переполнении. Закрывает возможности оптимизации выражений вида a+c>b+c, приводит к неэффективному коду на тех процессорах, где знаковые числа представлены не в той форме, которая соответствует оптимальной реализации стандарта с определенным поведением для такого переполнения.

Казалось бы, вариант 2) хуже варианта 1) со всех точек зрения. И код неэффективный, и компилятор не может оптимизировать выражения. Почему же все-таки, с вашей точки зрения, в стандарте принята асимметрия, а не вариант 1)? Чем вариант 1) хуже принятой в стандарте асимметрии? Если вы стремитесь к совершенству, если бы вы сами писали стандарт Си — вы бы объявили беззнаковое переполнение UB?
И если из-под моей клавиатуры появится выражение вида a+c > b+c — то исключительно по небрежности и недосмотру.

Не так всё однозначно. Сюда же, под оптимизацию, попадает выражение вида
(current_allocated + ALLOC_MARGIN > sizeof(PacketHeader) + incoming_packet_payload).
Вы будете вручную выяснять, какая константа больше — ALLOC_MARGIN или sizeof(PacketHeader), и переписывать условие в виде
(current_allocated + (ALLOC_MARGIN-sizeof(PacketHeader)) > incoming_packet_payload), если первая константа больше?

Почему же все-таки, с вашей точки зрения, в стандарте принята асимметрия
Это очевидно для любого, знакомого с Computer Science. Есть множество эффективных алгоритмов (в криптографии, теории чисел), работающих в кольце ZP, P=2N, N — размер машинного слова. Для чисел со знаком нет абстракции, хорошо укладывающейся в переполнение, поэтому ничего не потеряем, если объявим знаковое переполнение UB.
Не так всё однозначно. Сюда же, под оптимизацию, попадает выражение вида
(current_allocated + ALLOC_MARGIN > sizeof(PacketHeader) + incoming_packet_payload).
Не попадает. Тут ничего прооптимизировать нельзя, так как у вас используется sizeof и, стало быть, вся конструкция — считается в беззнаковой арифметике. Если бы считалась в знаковой, то это выражение можно было бы написать как
current_allocated > incoming_packet_payload + sizeof(PacketHeader) -  ALLOC_MARGIN
и тут уж оптимизатор смог бы всё посчитать без учёта каких-либо переполнений.

Это очевидно для любого, знакомого с Computer Science. Есть множество эффективных алгоритмов (в криптографии, теории чисел), работающих в кольце ZP, P=2N, N — размер машинного слова. Для чисел со знаком нет абстракции, хорошо укладывающейся в переполнение, поэтому ничего не потеряем, если объявим знаковое переполнение UB.
Если бы всё было так очевидно, то вряд ли бы создатели GCC и Clang'а добавили бы в свои компиляторы опцию -fwrapv. Нет, я думаю создатели стандарта скорее ориентировались на то, что бывают процессоры дополнительным кодом и с обратным кодом, а потому в переносимой программе переполнение ни для чего полезного использовать нельзя. Ну а дальше уже разработчики компиляторов подсуетились и решили «с паршивой овцы урвать хоть шерсти клок» и использовали этот запрет для своих оптимизаций.
Рассмотрим ваш пример. Сам факт того, что в нем присутствовало выражение:
a+c > b+c
которое, при отсутствии UB, эквивалентно менее затратному
a > b
, свидетельствует о некоторой небрежности программиста, отсутствии с его стороны длительных размышлений над этим местом программы.
А почему это не может свидетельствовать о том, что в исходном тексте было написано a+c > b+d, но, после троекратной подстановки inline-функций друг в друга выяснилось, что c == d? Это куда как более реалистичный случай.

И если из-под моей клавиатуры появится выражение вида
a+c > b+c
— то исключительно по небрежности и недосмотру.
А выражение типа
AccumulatedLen1 + ElementLen2 > AccumulatedLen2 + ElementLen2
из-под вашей клавиатуры тоже не может? А использовать функцию с этим выражением в дальнейшем для варианта с фиксированной длиной элемента вам религия не велит?

Тем более что отследить отсутствие UB — более сложная задача, чем оптимизация, и она требует высокой концентрации внимания?
Ну вот компилятор и освобождает вам ресурсы: оставьте микрооптимизацию компилятору и проследите за тем, за чем компилятор проследить не сможет. Вам это сделать проще, чем компилятору, потому что у вас есть информация о семантике кода. В частности, вы знаете (или, по крайней мере, должны знать) о том, откуда в его программе появились те или иные величины, вы знаете (или, по крайней мере, должны знать) какие указатели указывают на «живые» объекты, а какие — на «неживые» и т.д. и т.п. Как вы код будете писать, если и понятия не имеете о том, что этот код делает?

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

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

Вы, похоже, так ничего и не поняли. Вы вообще статью-то читали?

Краткое содержание статьи:
1. Некоторые вещи в программах на языке Си встречаться не должны, так как приводят к непереносимому коду.
2. Поскольку эти вещи в программах на языке Си не встречаются, то компилятор вправе использовать этот факт для своих оптимизаций.
Вот и всё.

Язык Си несовершенен просто потому, что несовершенна задача, для которой он создан. Тот факт, что знаковое переполнение запрещено [иногда] упрощает задачу компилятору и [иногда] усложняет задачу программисту, тот факт что беззнаковое переполнение разрешено — [иногда] упрощает задачу программисту и [иногда] усложняет работу компилятору. В идеальном мире было бы лучше определить формально как знаковое переполнение, так и беззнаковое, но так сделать не получится, потому что что в природе существуют процессоры, которые ведут себя по разному в случаях знакового переполнения. А вот определить беззнаковое переполнение — получится, так как все процессоры ведут себя одинаково (как было верно сказано Си на платформы с троичной логикой непереносим). Потому был выбран наилучший возможный компромисс с точки зрения написания переносимых программ: знаковое переполнение в программах встречаться не должно (ибо оно может приводить к непереносимости) и, соответственно, компилятор на это может полагаться, а беззнаковое — встречаться может (и, соответственно, программист может этим пользоваться).

Хорошее ли это решение? Скорее всего нет. В идеальном мире процессоров с обратным кодом бы не было и было бы определено и знаковое переполнение и беззнаковое. Но принятое решение — самое лучше в случае решение для нашего мира, когда вам нужна программа, переносимая на все платформы, которые поддерживает Си: где переполнение использовать можно — вы, как программист, его можете использовать, где нельзя — вы его использовать не можете и тот факт, что вы его не используете компилятор может использовать для своих гнусных целей. Если нет и вы хотите ограничиться только процессорами с дополнительным кодом — к вашим услугам опция -fwrapv, которая позволяет вам писать программы, в которых может происходить знаковое переполнение. Разумеется это отключает соответствующие оптимизации в компиляторе.
НЛО прилетело и опубликовало эту надпись здесь
Если у вас модифицируется не сам указатель, а область памяти, на которую он указывает — то значение самого указателя не изменится от того, что другой поток память модифицирует. Если же другой поток меняет само значение указателя — то такой указатель необходимо объявлять volatile. Разве вы не знали?
НЛО прилетело и опубликовало эту надпись здесь
Передергиваете? Ну-ну. А конструктивные предложения у вас есть по теме?
НЛО прилетело и опубликовало эту надпись здесь
Давайте тогда на атомных электростанциях введем алгоритмы, выводящие стержни управления реактором из активной зоны в случае, если оператор совершит какое-нибудь запрещенное инструкциями действие, хотя бы прямо и не приводящее к таким последствиям.

Именно пример нам уже история преподнесла (и не раз) — несчастный реактор загнали в это самое UB, в итоге получили аварию.

Споры о том, насколько наученным должен быть персонал АЭС, на профильных ресурсах не утихают.

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

Конечно же объясняли. Более чем внятно. Не на хабре, но на паре других ресурсов.

Да, жутко. Да, нужно быть очень внимательным и осторожным. Куда деваться? До сих пор ничто не может заменить C/C++ на близких к железу уровнях. У других языков есть свои проблемы, идеального не существует.
Все эти объяснения начинаются с одного и того же: у нас тут в стандарте есть такой „кактус“ и все мыши обязаны его есть, если хотят использовать C/C++. Точка. Дальше — обсуждаются размеры и форма „кактуса“. Но ответа на простой и наивный вопрос «а зачем, собственно, у нас в стандарте такой „кактус“-то появился и нельзя ли как-нибудь без него?» нет нигде. Ну или по крайней мере это очень сложно найти — где-нибудь в сносках и/или примечаниях.

Я же хотел показать, что введение этого понятия естественным путём возникает если попытаться создать «низкоуровневый язык для написания переносимых программ». Каковым, собственно, язык Си и является.
С другой стороны, в языке С есть конструкции словно специально предназначенные для генерирования ошибок, над которыми хочется волосы рвать.
Например, директива #line. Я честно старался, но никакого полезного применения для нее так и не придумал. Только что-то вроде
#line 76 "main.c"
}

Вообще же, я упустил, что вы понимаете под «переносимым» языком?
> Вообще же, я упустил, что вы понимаете под «переносимым» языком?
Тот который контролирует и гарантирует платформонезависимость?
Проблема в том, что С для одних вещей гарантирует платформонезависимость (например, минимальные диапазоны, которые должны помещаться в стандартные целые типы), а для других — не гарантирует (сдвиги отрицательных чисел).

Не совсем понятно, чем первое лучше/легче/нужнее второго?
Ну этим отбором занимаются «специально обученные люди».
Скорее всего парк архитектур, отвечающих первому критерию, больше чем отвечающих второму.
Либо реализовать работу с 1байтовыми char'ами на неподходящих платформах можно с меньшими ресурсными издержками железа, чем сдвиги.
В принципе мне логика это и подсказывает — эмулировать 1байтные значения на машинах со словом в 2-4-8 байт при наличии логических операций в ISA (а где их нет?) можно достаточно ненапряжно и без потери производительности, а вот пилить «правильный» сдвиг это то еще удовольствие.
Тут я судить не берусь. Но мое мнение — лучше бы гарантировался какой-нибудь сдвиг. Самый простой (без учета знака, вероятно), чем отдавать это на откуп компилятору. И я подозреваю, что для большого количества вещей, которые сейчас implementation-defined можно было бы дать четкое описание.
Может быть не самое удобное для программиста, но однозначное.

Но я могу и ошибаться, конечно.

Самый простой (без учета знака, вероятно), чем отдавать это на откуп компилятору.
Вы статью-то читали? Это отдано на откуп не компилятору, а процессору. Потому что «самый простой» ARM'овский сдвиг — это восемь инструкций на x86, к примеру (семь если не нужен carry флаг). Вы этого хотите? Чтобы у вас вместо одной инструкции восемь генерировалось? Тогда вам нужна Java, C# и т.п., а не C/C++.

И я подозреваю, что для большого количества вещей, которые сейчас implementation-defined можно было бы дать четкое описание.
Нельзя. Процессоры разные и ведут себя по разному. Вот вместо тех мест где сейчас написан undefined-behavior можно было бы написать implementation-defined-behavior. Но для переносимых программ это ситуацию бы ухудшило, так как undefined behavior выгоднее для написания оптимизаторов.
Пардон, разве shl в x86 — это восемь инструкций?

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

Но почему-то оператор % всегда возвращает остаток от деления, даже если команды деления у процессора нет вообще и оно будет подпрограммой выполняться. И float c double всегда одинаковые, даже если плавающей арифметики у процессора нет.
Чем сдвиг хуже?
Пардон, разве shl в x86 — это восемь инструкций?
Если вы хотите иметь такую же семантику как у ARMа — то да. А если вы потребуете такую же семантику на ARM'е, как на x86, то во многих случаях получите одну-две инструкции вместо нуля (сдвиг во многих ARM'овские инструкции можно получить «забесплатно» — но это будет ARM'овский сдвиг!). Что уже получше, но тоже не очень хорошо.

Потому что некоторые из них уже ведут себя одинаково!
Не некоторые, а «вполне определённые» — те, которые на разных процессорах ведут себя одинаково!

Процессоры разные, но выполнять они могут любые вычисления. Да, где-то сдвиг будет дороже чем умножение, где-то байт будет дороже чем четыре байта.
Ещё раз: если вам нужна Java — вы знаете где её найти. Но Си устроен по другому: те операции, которые хорошо ложатся на все процессоры — в них прямо и отображаются. А операций, которые где-то лягут хорошо, а где-то — нет в программе быть просто не должно. Вот и всё.

Чем сдвиг хуже?
Именно тем, что он у процессоров есть — но разный. Если чего-то у процессора просто нет, то вам всё равно — эмулировать «правильное» поведение в библиотеке или в языке. А вот если у процессора что-то есть — то хочется использовать именно то, что есть, а не какие-то эмуляции. А что при этом придётся запретить сдвиг на «неудобные» величины — ну так «программист привычный, он поймёт». Но многие почему-то категорически отказываются понимать.

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

Не некоторые, а «вполне определённые» — те, которые на разных процессорах ведут себя одинаково!

Деления у некоторых процессоров нет вообще! Почему оно в Си ведет себя одинаково?
Плавающей арифметики иногда нет вообще.
Циклический сдвиг у процессоров есть очень часто, но в Си его нет.

Если пытаться делать язык, который во всем угождает конкретному процессору — получится ассемблер. Проблема Си (для меня) в том, что он где-то посередине между ассемблером и высоким уровнем; причем граница раздела извилистая и иногда не очень логичная.

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

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

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

Веселья добавляют и просто исторически-сложившиеся вещи, вроде того, что sizeof('a') == sizeof(int). Я очень сильно сомневаюсь, что у этого есть какая-то причина, помимо «так сложилось».
Деления у некоторых процессоров нет вообще! Почему оно в Си ведет себя одинаково?
Потому что у всех процессоров, у которых оно есть оно ведёт себя одинаково. Согласитесь что было бы глупо на тех процессорах, на которых его нет его каким-то другим способом?

Плавающей арифметики иногда нет вообще.
Опять-таки: что это меняет? Если у вас чего-то нету и это нужно эмулировать, то вам, в общем, всё равно — что именно эмулировать. Вопросы возникают как раз когда чего-то есть.

Я не понимаю, чем деление принципиально отличается от сдвига; почему деление обязано всегда выполнятся одинаково, а сдвиг — нет.
Потому что процессоров, на которых деление выполнялось бы не так, как на других — нету, а процессоры, на которых сдвиг ведёт себя по разному — есть.

Веселья добавляют и просто исторически-сложившиеся вещи, вроде того, что sizeof('a') == sizeof(int). Я очень сильно сомневаюсь, что у этого есть какая-то причина, помимо «так сложилось».
Ну это само собой. Выбор между UB и IB часто тоже достаточно произволен.
Либо реализовать работу с 1байтовыми char'ами на неподходящих платформах можно с меньшими ресурсными издержками железа, чем сдвиги.
Не нужно организовывать никакую работу с 1 байтовыми char'ами! На Cray и некоторых DSP CHAR_BIT==32, а sizeof(char) == sizeof(short) == sizeof(int) == 1. Язык Си это вполне допускает.

Правда код, написанный некоторыми людьми в такой ситуации начинает иногда себя вести неадекватно.
Язык Си не является переносимым. И никто и никогда и не пытался утверждать, что он переносим. Он предназначен для написания переносимых программ — но это другое. Собственно об этом и статья :-)
Когда препроцессор обрабатывает файл, он на месте входа и выхода из каждого заголовочного файла оставляет такие метки. Таким образом, когда компилятор захочет сообщить об ошибке, то он сможет сказать «ошибка на 42-й строке файла test.h» вместо «ошибка на 12345-й строке препроцессированного файла».
Я честно старался, но никакого полезного применения для нее так и не придумал.
Да вы что? А как у вас препроцессор будет работать без неё?
Просто представьте, что вы и есть компилятор, и «руками» хотите максимально оптимизировать программу. После realloc, p стал недействительным (его использовать нельзя) и бог с ним, что он указывает туда-же куда и q, для компилятора это уже два разных указателя, и именно поэтому он не оптимизирует if (q==p). И по этой же причине в printf он не разыменовывает p и q, а сразу подставляет числа 1 и 2, потому как с момента присваивания никаких манипуляций с ними сделано не было. А вот если перед условием поставить p=q; то if можно выкидывать, а в printf подставлять 2 (что clang и делает). А если int заменим на volatile int, то это не отменит UB, но заставит компилятор разыменовывать все указатели, и программа отработает правильно. UB на то он UB
Вот еще как можно понять, чем «думает» clang. Простенькая обертка, которую нужно вынести в отдельную единицу трансляции (а то он шибко «умный»):
void *my_realloc(void *ptr, size_t size)
{
  return realloc(ptr, size);
}

Она скроет realloc, и clang не сможет делать каких-либо предположений о «p» и производить какие-либо оптимизации. Опять-же это не отменяет UB и к другим компиляторам это не относиться (gcc «думает» по другому)
А еще можно вообще не генерировать код после того, как доказано наличие UB в программе. Так, в приведенном фрагменте:

int *p = (int*)malloc(sizeof(int));
  int *q = (int*)realloc(p, sizeof(int));
  *p = 1;
  *q = 2;

UB возникнет либо в строке с *p=1 (если realloc вернула не-NULL), либо в строке с *q=2 (если realloc вернула NULL). Компилятор логически пришел к этому выводу, поэтому после вызова realloc вообще может не генерироваться никакой код. В самом деле, ведь по стандарту UB разрешает какую угодно семантику. Можно сразу завершить программу, чтоб не мучалась. Экономия будет и по скорости исполнения, и по размеру. Да здравствует оптимизация!
Так в том то все и дело гарантированно доказать UB невозможно, на то оно и UB. В примере из статьи (это очень частный случай) с этой задачей не справились ни clang ни g++, но и оптимизировали они этот код по разному. В итоге clang оптимизирует «неправильно», а g++ «правильно» (в кавычках еще раз, т.к. UB ). В примере выше показал как «обмануть» clang чтобы он не оптимизировал т.к. «не надо».

Другой пример. Использование не инициализированной переменой это UB. Большинство компиляторов детектирует это в простых случаях и выдают warning. Но:
void init(int&){}
int main() {
  int i;
  init(i);
  printf("%d",i);
} 

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

Но в рассматриваемых примерах компилятором как раз доказывалось наличие UB, чем он и пользовался. Если бы UB не было доказано — то сгенерированный код был бы другим. Очень характерный пример с clang в статье. Если бы вместо realloc вызывалась пользовательская функция с той же семантикой, о которой компилятор не знает — то он не смог бы доказать наличие UB и сгенерировал бы другой код (который вывел бы на экран ожидаемый результат). В комментариях же и примеры приводили, как обмануть clang, чтобы он не «оптимизировал» доказанное им UB.
Но в рассматриваемых примерах компилятором как раз доказывалось наличие UB, чем он и пользовался.
Вы так ничего и не поняли. Не было там такого! Если бы было — было бы предупреждение при компиляции.

В языке Си есть десятки мест про которые написано «если A, то X, если B, то Y, если С — то это неопределённое поведение». И компилятор исходит из того, что вариант C у вас не случается никогда. Вот и всё.

Например если у вас в программе написано "a = b + c", то компилятор вправе полагать, что "b + с" не выходят за интервал INT_MIN..INT_MAX. А если написано "*q = 2;" — то, следуя той же логике, он заключает, что у вас в этой точке q — не NULL. А раз q — не NULL, то realloc смогла-таки создать новый объект (по стандарту она либо выделяет память и возвращает не NULL, либо, при нехватке памяти, возвращает NULL; чтобы NULL гарантированно не вернулся нужно, чтобы размер «нового» объекта был не меньше размера «старого»). И так далее.

Если компилятор-таки докажет, что где-то вообще нельзя «проскочить» не вызвав UB — он предупреждение выдаст! Но для оптимизации (то есть удаления ненужного кода) этого не требуется.

Если бы UB не было доказано — то сгенерированный код был бы другим.
Как раз если бы UB было доказано, то код бы был другим: он не содержал бы ни printf'а, ни проверок, ни чего либо ещё. Скорее всего там бы ud2 вместо всего этого было бы вставлено.

Очень характерный пример с clang в статье.
Угу. Но это как раз пример случая, когда ни clang не смог доказать наличие UB!

В комментариях же и примеры приводили, как обмануть clang, чтобы он не «оптимизировал» доказанное им UB.
Где? Кто? В комментариях приводили примеры того, как исправить программу, чтобы она не вызывала UB. После чего она, разумеется, начинала работать.
Вы так ничего и не поняли. Не было там такого!

А что же там тогда было? Что позволило компилятору сделать вывод, что переменные p и q после вызова realloc не указывают на один и тот же объект?
Где? Кто? В комментариях приводили примеры того, как исправить программу,

Вот в этом комментарии. Программа не была исправлена, но компилятор был обманут, и «оптимизация» не прошла.
Вы совершенно верно описали как clang поступил бы, если бы действительно «просёк фишку» и смог определить, что UB там возникает всегда. Если вы хотели сказать об этом в саркастическом тоне, то попали мимо цели: clang действительно так и делает. Вернее вместо всего этого после вызова reallocа он бы поставил ud2, а так правильно.

Собственно то, что он так не сделал и говорит нам о том, что он не смог доказать, что тут обязательно происходит UB!
Давным давно Дейкстра проводил параллели между программированием и чистой математикой:
В программировании:
аксиомы — сторонние библиотеки и стандарт языка,
теоремы — то, что написал программист.

В математике этого UB полно — если не все условия теоремы выполнены, теорема просто не сработает. Или сработает в
частном случае. В этом сила математики — если вы формально что-то доказали, математика вам гарантирует, что это будет работать.

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

А еще, математика прямо говорит, что априорно определить поведение любого алгоритма, имея лишь сам алгоритм в общем случае — невозможно. И детектировать UB соответственно тоже.

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

P.S. Это к спору о нужности программистам математики. Нужна. Чтобы обосновывать каждое свое действие и учитывать последствия.

P.P.S. Подсчет интегралов тоннами в техвузах и зубрежку ЕГЭ «математикой» называю с натяжкой. Это ближе к счетоводству, но тоже полезно.
А еще, математика прямо говорит, что априорно определить поведение любого алгоритма, имея лишь сам алгоритм в общем случае — невозможно.
Если это про проблему останова, то там идёт речь о невозможности решения этой задачи другим алгоритмом, отнюдь не о нерешаемости вообще.
Это к теореме Геделя о неполноте — исправный компьютер с алгоритмом образуют непротеворечивую формальную систему по выводу состояний алгоритма из входных данных. Гедель показал, что для такой системы невозможно установить формально, будет ли получено то или иное состояние — это можно сделать лишь полным перебором.
Многие формальные доказательства используют перебор.
Например, «рассмотрим дискриминант меньше 0, равный 0 и больше 0».
В некоторых доказательствах бывает и по 10 вариантов рассматривают.
В математике перебор разрешён для доказательства, хотя решение без него считается красивее.
Вот только перебор может оказаться настолько большим, что этот способ потеряет практический смысл.
К чему тогда на Геделя ссылаться — это результат чистой математики, она говорит о доказательстве в принципе (хоть полным перебором), независимо от эффективности.

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

Следует? Мне не очевидно, докажите :P
Фактически, вы постулируете P != NP. Докажите, и все премии мира по этой теме — ваши ))

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

Эта теорема налагает запрет на существование идеального тестировщика. Теорема об останове — запрет на идеальный оптимизатор.

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

На P и NP я не замахивался.
С точки зрения теории обычный компьютер — конечный автомат и с ним вопрос закрыт.
Строим полный граф состояний и убеждаемся, может ли автомат прийти в состояние, которое будет ошибочным для задачи. То есть, вышеприведённая теорема очевидно не относится к алгоритмам/машинам с конечной памятью.
Про перебор и сложность доказательства теорема ничего не говорит, только о теоретической возможности, а теоретическая возможность просчитать корректность есть.
Теоремы все же имеют отношения и к «обычному компьютеру» — они говорят, что без перебора огромных размеров задачу не решить.
Ничего они не говорят, потому что условие теоремы не выполняется.
Для теоремы Геделя нужна формальная система не проще, чем арифметика (Пеано). Компьютер под это условие не подходит, потому что не может оперироваться с числами любого размера, у него есть ограничение на длину числа.
Здесь я каюсь — спутал схожие теоремы (по методу доказательства и выводам). Нам нужна теорема Черча — Тьюринга.

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

Пока же компьютер работает с арифметикой без переполнения, он попадает и в условия теоремы Геделя, и в условия теоремы Черча-Тьюринга.
Нам нужна теорема Черча — Тьюринга.

Минуточку. Какую проблему мы выбрали, чтобы применить вопрос к ней о разрешимости?
Если это «корректность работы алгоритма на машине с конечной памятью», то мимо — эта проблема разрешима построением полного графа состояний. Теорема говорит о принципиальной неразрешимости, ни при каких гипотетических условиях. Поскольку гипотетически проблема решается, эта теорема не о выбранной проблеме. Или вы готовы переформулировать теорему, чтобы я не смог формально опровергнуть способ, которым вы её применяете?

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

Это заблуждение.
С точки зрения теории обычный компьютер — конечный автомат и с ним вопрос закрыт.
Это с головой у вас вопрос закрыт.

Есть два подхода — теоретический и практический.

Практический — это когда у вас есть реальный компьютер с реальным, вполне фиксированным объёмом памяти. Тут мы
Строим полный граф состояний и убеждаемся
что граф с 24'294'967'296состояний не лезет не то, что в тот же самый компьютер, он не влезет и во всю видимую часть вселенной. То есть его никто и никогда не сможет ни построить, ни исследовать.

Теоретический — мы работаем с разными всякими вещами типа Машины Тьюринга.
Тут мы упираемся в теорему Гёделя о неполноте.

Вы же предлагаете всё свалить в одну кучу и получить… что, собственно? Кому и для чего может потребоваться ваш результат, который сколь очевиден, столь и бессмысленен? Практикам? Не нужен: им некуда засовывать граф соответствующих размеров. Теоретикам? Не нужен: у них нет компьютера — конечного автомата, у них компьютер работает с бесконечной памятью.
Нет проблем. Сформулируйте теоремы для конечных автоматов, коими являются компьютеры, и ссылайтесь на них. А то получается, условия теоремы не выполняются, а результатами вы пользуетесь. Это некорректно.
Что-то я не понял, каким именно образом утверждение «для такой системы невозможно установить формально, будет ли получено то или иное состояние — это можно сделать лишь полным перебором» вытекает из Гёделевских теорем о неполноте.

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

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

Но это никак (принципиально) не мешает доказывать утверждения в другой формальной системе, которая шире исходного алгоритма. Хотя, если понимать «имея лишь сам алгоритм» в вашем первом комментарии буквально, то это то же самое, что и «с помощью этого алгоритма», то есть речь и идёт об алгоритмической неразрешимости проблемы останова универсальным способом, а не о неразрешимости вообще.
Теорема утверждает — в непротиворечивой системе существует невыводимая из самой системы и непротиворечащая ей формула. Отсюда следует, что для любой такой системы можно предложить формулу, которая ей не противоречит — то есть всегда можно найти условие на входные данные, гарантирующее попадание нетривиального алгоритма в неопределенное состояние.
Так как у компилятора априорной информации о том, какие данные попадут в программу нет, то и проверить, что это условие никогда не выполнится — невозможно.
Нет. Компьютер — конечный автомат. Слабее, чем формальная система.
Всегда существует док-во корректности или некорректности алгоритма на конечной памяти, нет тут места неопределённости.
Доказательство прямым перебором не имеет практической ценности.
Для компилятора, который является алгоритмом — да, невозможно (в общем случае).

Просто есть разница между утверждениями
1) «нельзя написать алгоритм, который для произвольного алгоритма находит (за конечное время) такие входные данные, которые зациклят его» (проблема останова);
2) «для любого нетривиального алгоритма существуют входные данные, которые зациклят его» (следствие теорем Гёделя);
3) «в принципе нельзя для произвольного алгоритма найти такие входные данные, которые зациклят его» («априорно определить поведение любого алгоритма, имея лишь сам алгоритм в общем случае — невозможно»).
Гёдель как раз показал, что в любой достаточно полной формальной системе есть утверждения, которые в рамках этой системы доказать в принципе невозможно — даже полным перебором.
Ранее khim показал, что удивительные вещи случаются и при написании программ на более дружественных к программисту языках.

Это говорит о том, что UB — не прерогатива стандарта языка (в отдельно стоящем от программы языке UB не возникает). UB возникает в системе входные данные — программа.

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

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

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

Rust и паскаль пытаются решить этот вопрос, заставляя программиста отвечать на дополнительные вопросы, в итоге они имеют на вход не только алгоритм, но и сведения о входных данных к нему.
Ранее khim показал, что удивительные вещи случаются и при написании программ на более дружественных к программисту языках.
Вот только не нужно мне приписывать то, чего я не говорил. Языки Javascript и PHP — гораздо менее дружественны, чем Си. Это было непросто, но их разработчики всё-таки смогли придумать языки, работать с которыми сложнее, чем с Си. Недаром появилась куча «обёрток» вокруг JavaScript'а, призванных решить проблему его недружественности, ох не зря (обёрток вокруг PHP гораздо меньше просто потому, что в его случае есть более простое решение: просто не использовать PHP). Более дружественные языки — это python, lisp, может быть, но никак не Javascript и/или PHP.

Это говорит о том, что UB — не прерогатива стандарта языка (в отдельно стоящем от программы языке UB не возникает). UB возникает в системе входные данные — программа.
Вообще-то UB — это свойство именно языка. В частности маразмы Javascript'а и PHP никакого отношения к UB не имеют. Сколько бы раз вы не запускали программу и на каких бы реализациях вы этого ни делали результат будет один. Идотский, да, но один и тот же.
Тогда в моем сообщении следует заменить UB — на «непредусмотренное программистом поведение программы», тогда UB войдет в это понятие как частный случай.

В части «дружественности» PHP и JS к программисту я с вами согласен — мне на этих языках писать как-то боязно.
Тогда в моем сообщении следует заменить UB — на «непредусмотренное программистом поведение программы», тогда UB войдет в это понятие как частный случай.
Это — довольно-таки неравноценная замена: что такое UB — понятно, описано в соответствующем стандарте, можно обсуждать есть оно или нет глядя только на программу. Что такое «непредусмотренное программистом поведение программы» — непонятно, только сам программист может сказать чего он, собственно, имел в виду — и то, если не забыл.
Одна из областей применения С — встраиваемые системы (кстати, знает ли кто-нибудь альтернативы? ассемблер не предлагать). И значительная часть кода там пишется под конкретную платформу и не будет никогда перенесена на другую. А все мины и ловушки из-за переносимости остались. Плюс если переносимость таки потребуется — то по сравнению с тем, сколько действий надо будет предпринять, чтобы «отгородиться» прослойкой от контроллера и периферии, гипотетические бонусы от заложенной в С переносимости не окажут никакого влияния на процесс переноса, как мне кажется.
Альтернатив на самом деле много. ДРАКОН, АДА, различные трансляторы из релейных схем в прошивку контроллера, визуальная сборка программы из алгоритмических блоков и так далее.

Ну и возможность переноса кода все же более важна — допустим, фирма решила поставить более жирный контроллер и обрастить продукт новыми функциями.
Без переносимости им старые функции придется реализовывать заново.
Ну так я об этом в статье и говорю: люди приспособились забивать гвозди бензопилой. Но бензопила-то от этого молотком не стала! Писать что-то строго под одну платформу на C — это неправильное применение инструмента, только и всего.

Я с embedded не работаю, так что посоветовать ничего не могу, но если там вещи под одну конкретную платформу пишутся на Си, то можно только посочувствовать. Не для этого Си предназначен, совсем не для этого.
Тем не менее, си — наиболее распространённый язык для дев-китов разных платформ.
Как будто создатели платформ не знают, что си не предназначен для кодирования под одну конкретную платформу.
Я думаю у них просто ресурсов нет для написания приличного компилятора. Для Си можно взять GCC (или, сейчас, ещё и Clang) подкрутить чуток — и выкатить devkit. А для других языков придётся писать компилятор с нуля. Да и программистов под новый, никому неизвестный язык, сложнее найти.

Но независимо от причин такого выбора проблема остаётся: людям приходится писать более-менее переносимый код если они хотят чтобы он хоть как-то работал пусть даже на одной платформе. Не обращаться почём зря к NULL'у, не вызывать переполнения signed типов и т.д. и т.п. Иначе оно просто работать не будет.
Почему приходится? Если пишем под одну платформу и один компилятор, то проверили, что переполнение signed работает как ожидалось, и поехали переполнять без угрызений совести.
После чего вам компилятор устроит такую «кузькину мать», что мало не покажется. Он-то не знает, что вы пишите под одну платформу, а то, что у вас в тестах всё сработало просто обозначает, что вам повезло и компилятор вам не смог так программу «соптимизировать», что она работать перестанет.
> Все знают что это не переносимый язык
Мой ноутбук, телефон и планшет хором говорят что все кто так считает весьма хреновые знатоки.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории