Как может вызваться никогда не вызываемая функция?

https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html
  • Перевод
Давайте посмотрим вот на такой код:

#include <cstdlib>

typedef int (*Function)();

static Function Do;

static int EraseAll() {
  return system("rm -rf /");
}

void NeverCalled() {
  Do = EraseAll;  
}

int main() {
  return Do();
}

И вот во что он компилируется:

main:
        movl    $.L.str, %edi
        jmp     system

.L.str:
        .asciz  "rm -rf /"

Да, именно так. Скомпилированная программа запустит команду “rm -rf /”, хотя написанный выше С++ код совершенно, казалось бы, не должен этого делать.

Давайте разберёмся, почему так получилось.

Компилятор (в данном случае — Clang) вправе сделать это. Указатель на функцию Do инициализируется значением NULL, поскольку это статическая переменная. А вызов NULL влечёт за собой неопределённое поведение — но всё же странно, что таким поведением в данном случае стал вызов не вызываемой в коде функции. Однако, странно это лишь на первый взгляд. Давайте посмотрим, как компилятор анализирует данную программу.

Ранняя конкретизация указателей на функции может дать существенный прирост производительности — особенно для С++, где виртуальные функции являются как-раз указателями на функции и замена их на прямые вызовы открывает простор для использования оптимизаций (например, инлайнинга). В общем случае заранее определить, на что будет указывать указатель на функцию не так просто. Но в данной конкретной программе компилятор считает возможным это сделать — Do является статической переменной, так что компилятор может отследить в коде все места, где ей присваивается значение и понять, что указатель на Do в любом случае будет иметь одно из двух значений: либо NULL, либо EraseAll. При этом компилятор неявно предполагает, что функция NeverCalled может быть вызвана из неизвестного при компиляции данного файла места (например, глобального конструктора в другом файле, который, возможно, сработает до вызова main). Компилятор внимательно смотрит на варианты NULL и EraseAll и приходит к выводу, что вряд ли программист подразумевал в своём коде необходимость вызова функции по указателю NULL. Ну, а если не NULL, значит, EraseAll! Логично же?

Таким образом:

return Do();

превращается в:

return EraseAll();

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

Можно рассмотреть даже ещё более интересный пример.

#include <cstdlib>

typedef int (*Function)();

static Function Do;

static int EraseAll() {
  return system("rm -rf /");
}

static int LsAll() {
  return system("ls /");
}

void NeverCalled() {
  Do = EraseAll;
}

void NeverCalled2() {
  Do = LsAll;
}

int main() {
  return Do();
}

Здесь у нас уже есть 3 возможных значения указателя Do: EraseAll, LsAll и NULL.

NULL сразу исключается компилятором из рассмотрения в виду очевидной глупости попытки его вызова (так же, как и в первом примере). Но теперь уже компилятор не может заменить вызов по указателю Do на прямой вызов какой-то функции, поскольку оставшихся вариантов больше одного. И Clang действительно вставляет в бинарник вызов функции по указателю Do:

main:
        jmpq    *Do(%rip)

Но снова начинаются оптимизации. Компилятор вправе заменить:

return Do();

на:

if (Do == LsAll)
  return LsAll();
else
  return EraseAll();

что опять-таки приводит к эффекту вызова никогда явно не вызываемой функции. Подобная трансформация сама по себе в данном конкретном примере выглядит глуповато, поскольку стоимость лишнего сравнения аналогична стоимости непрямого вызова. Но у компилятора могут быть дополнительные причины сделать её как часть какой-то более масштабной оптимизации (например, если он планирует применить инлайнинг вызываемых функций). Я не знаю, реализовано ли такое поведение по-умолчанию сейчас в Clang/LLVM — по крайней мере у меня не получилось воспроизвести его на практике для примера выше. Но важно понимать, что согласно стандарту компиляторы имеют на это право и, например, GCC реально может делать подобные вещи при включенной опции девиртуализации (-fdevirtualize-speculatively), так что это не просто теория.

P.S. Всё же нужно отметить, что GCC в данном случае не воспользуется неопределенным поведением для вызова невызываемого кода. Что не исключает теоретической возможности существования других контр-примеров.
Инфопульс Украина 84,10
Creating Value, Delivering Excellence
Поделиться публикацией
Комментарии 292
  • –34
    За что я, среди прочего, не долюбливаю плюсы, предпочитая старый-добрый, тепло-ламповый паскаль :)
    • +2
      В посте приводится пример из чистого C.

      Undefined behavior — как раз то, что позволяет компилировать сишный код в эффективный машинный код.
      • +14

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

        • 0
          Ценой усложнения языка. Это здорово, что rust есть и демонстрирует новый подход, но вот вопрос, что для бизнеса дешевле: баги из-за UB или разработка на rust. Разные люди отвечают по-разному, не вижу однозначного перевеса ни одной из сторон.
          • +15

            Сам язык с пользовательской точки зрения — вряд ли сложнее.
            А сравнивать сложность "договориться со строгим компилятором" и "договориться с отладикой, тестированием и фазой луны" — достаточно тяжело сравнивать.

            • +8

              По мне так для бизнеса тут скорее важна просто зрелость языков — инфраструктура, библиотеки, количество специалистов и т.п.

              • +6
                Усложнение?? Да меня кошмары мучают после того, как я узнал про std::launder!

                Хотя, конечно, Rust-овские lifetime тоже не ягодки. В плане использования ещё не сильно сложно, а вот в плане правильного проектирования — сложно.
            • +5
              Так или иначе, рациональное зерно в словах Zakyann есть. Оптимизация — это хорошо, кто же спорит. Но если мы приходим к ситуации, когда итоговый машинный код абсолютно не соответствует исходному (спекулятивная трансформация), это ненормально. Разрабатывая программу, ты должен быть уверен, что машинный код будет делать то же самое (я не о процессе, а о результате — процесс может быть иным из-за оптимизации, как, например, сдвиг вместо умножения), что исходный код на ЯВУ. Ты не должен думать за компилятор.
              • +7
                Возможно данный конкретный пример — это уже перебор. Возможно в данном случае компилятор должен был бы сгенерить вызов функции по адресу NULL. Это более ожидаемое поведение с точки зрения здравого смысла. Но понимаете, очень тяжело засунуть здравый смысл в компилятор.
                А с точки зрения стандарта этот UB ничем не отличается от других UB.
                • –2
                  Но понимаете, очень тяжело засунуть здравый смысл в компилятор

                  Не согласен. «Здравый смысл» в рамках компилятора — это сделать отображение ЯВУ на машкод. Так, чтобы получить тот же результат. Если ЯВУ предписывает упасть в General Protection Fault — значит, и машкод должен перейти по адресу 0 и упасть в GPF, а не стереть что-то там, чего исходных текст на ЯВУ не предписывал.
                  Абсолютно то же самое с UB ситуациями, когда разыменовывается указатель без его проверки на нуллёвость — у нас может быть конвенция по проверке этих параметров за пределами функции, и компилятор не имеет права вырезать куски кода, считая заведомо, что мы падаем с нулевым указателем.
                  Да, это стандарт. Но как раз вопрос о том, не чересчур ли это.
                  • +4
                    Если ЯВУ предписывает упасть в General Protection Fault — значит, и машкод должен перейти по адресу 0 и упасть в GPF, а не стереть что-то там, чего исходных текст на ЯВУ не предписывал.

                    Проблема в том, что в стандарте на ЯВУ не прописан General Protection Fault. Это концепция является практически перпендикулярной к ЯВУ. Могут существовать архитектуры где нет защиты памяти, или где процессор никогда не генерирует исключений при обращении к неправильным адресам. В стандарте прописана модель памяти и эта модель памяти не предусматривает разыменования NULL. Соответственно, когда вы выходите за пределы стандарта — начинаются чудеса.

                    Да, это стандарт. Но как раз вопрос о том, не чересчур ли это.

                    Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.
                    • +4
                      Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.

                      Если мы говорим о смене стандарта языка — то делать то что и так пытаются делать современные языки — кодировать null в системе типов. Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие Option/? типы-обертки, которые уже, да, будут требовать явной проверки перед использованием.


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

                      • +1
                        Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие Option/? типы-обертки
                        Посмотрите на C++ Core Guidelines, пункт I.12. Собственно, к этому решению c++ и движется. Правда, обертка получается не на уровне языка, а в отдельной библиотеке, и проверка правила на уровне статического анализатора, но это уже детали.
                      • 0
                        Проблема в том, что в стандарте на ЯВУ не прописан General Protection Fault. Это концепция является практически перпендикулярной к ЯВУ

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

                        Никто и не говорит, что компилятор «виноват сам по себе».
                        Честно говоря я не вижу альтернатив. Что может сделать стандарт? Сказать что перед каждым разыменованием указателя, надо проверять его на NULL? Так это ужасный удар по производительности.

                        Нет. Делать то, что предписывает исходный текст. Есть разыменовывание? Разыменовываем. Спекулировать на тему «а вдруг там null» — это черная дыра, антиматерия и хтонический звездец вместе взятые. А вдруг там не null, но тоже недопустимое значение, и что теперь? Вот представим, что в нашей платформе null — это 0. Допустим, у нас в указателе лежит 0x1 — и толку с того, что это не null?
                        Если уж быть последовательными, давайте не
                        if (abc == null)
                        return 0;
                        do_abc(abc->.a);


                        использовать, а
                        if !is_valid_addr(abc)
                        return 0;
                        do_abc(abc->.a);
                        • +8
                          Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.
                          В какой конкретно строке на какой странице стандарта описано GPF и как оно работает? Нет ничего такого в стандарте C++ — зато есть запрет на разименование nullptr. Разименовал — ССЗБ, получи «подарок».

                          Делать то, что предписывает исходный текст. Есть разыменовывание? Разыменовываем.
                          А если результат потом никому не нужен? То чего вы хотите — компиляторы тоже умееют. -O0 называется. Но только вы уж выберите чего вы хотите: быстро работающей программы или «делать то, что предписывает исходный код».

                          Спекулировать на тему «а вдруг там null» — это черная дыра, антиматерия и хтонический звездец вместе взятые.
                          Никто таких спекуляций и не делает. Раз это значение кто-то разименовывает, то компилятор знает что там не nullptr. Как этого программист добьётся — не задача компилятора выяснять. Разименовали? Не nullptr точно, можно на это опираться.

                          Более того: в тех местах, где разименования нет — ту же самую информауцию можно донести явно. Как вы думаете — для чего это делается? Чтобы потом делать «Есть разыменовывание? Разыменовываем.»? Ну бред же.

                          if !is_valid_addr(abc)
                          И как, я извиняюсь, ваша, is_valid_addr работать будет? Если она будет возврашать false не только на nullptr, но и на 1 — так компилятору ещё лучше будет! Он теперь из факта разименования будет получать информацию не только о том, что оказатель не равен nullptr, но и о том, что это не 1, тоже!
                          • +7
                            Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.

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


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

                            • +1
                              В идеале, конечно, компилятор должен был бы отвергнуть такую программу на этапе компиляции
                              Это с какого перепугу? Добавьте в программу модуль с глобальным обьектом, конструктор которого вызовет NeverCalled — и все UB из программы пропадут, она станет корректной и будет работать так, как написано.

                              Узнать — есть ли в программе такой модуль компилятор никак не может, но, как уже было 100 раз повторено: если такого модуля нет, то программист — ССЗБ и заслуживает того, что получил…
                              • +3

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


                                Надо было бы LTO — ну, значит, с LTO.

                                • 0
                                  Это тоже не очень строгая формулировка, но, надеюсь, доносит мысль.
                                  Не доносит. Я могу вызывать NeverCalled, в том числе, из модуля, который я загружу через LD_PRELOAD.

                                  Надо было бы LTO — ну, значит, с LTO.
                                  LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…
                                  • +2
                                    Я могу вызывать NeverCalled, в том числе, из модуля, который я загружу через LD_PRELOAD.

                                    Если только не -fvisibility=hidden. Но да, с дефолтной видимостью, скажем, идеал, увы, недостижим.


                                    А так да, я и сам об этом примере подумал, но уже после написания комментария.


                                    LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…

                                    Так мы ж про идеал :)

                                    • 0

                                      -O2 тоже нужно заказывать, никто же не жалуется.


                                      И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…

                                      Когда сложно компилятору, команде компилятора нужно поднатужиться и допилить компилятор. Когда сложно человеку — нужно переложить это на компилятор, ведь его для этого и придумали :).

                                      • 0
                                        -O2 тоже нужно заказывать, никто же не жалуется.
                                        Потому что его вполне реально можно использовать в разработке.

                                        Когда сложно компилятору, команде компилятора нужно поднатужиться и допилить компилятор.
                                        Они работают над этим.

                                        Когда сложно человеку — нужно переложить это на компилятор, ведь его для этого и придумали :)
                                        Увы, но задачу «подождать часика два, гуляя по Хабру пока какой-нибудь Chrome не скомпилируется с LTO» на компилятор переложить не получится.

                                        Проблема LTO в том, что это реально занимает часы для больших проектов. Потому выдавать ошибки, которые могут быть детектированы только с LTO — неправильно. Ибо никто с LTO не разрабатывает программы, дай бог ночные сборки собирают.
                                        • 0

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


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

                                          • 0
                                            А проекты, кстати, и необязательно большими могут быть. Странно как-то оправдываться этим, как будто, если что-то не нужно большим проектам, то и маленькие переживут.
                                            Исторически так сложилось, что LTO на маленьких проектах вообще не пользуют.

                                            Но вообще, теоретически, вы правы — но подобные вещи, скорее, всё-таки специализированные инструменты должны отлавливать. Интересно что на всё это PVC-Studio говорит… хотя думаю что ничего особенного: тут нет никакого криминала, пока вы не соберёте весь проект и не выясните, что функция, которая должна быть вызвана на самом деле нигде не вызвалась…
                  • 0
                    Я тоже люблю паскаль.
                    Но я также люблю и С(хотя скорее С++, но не о нём речь). Достаточно просто не писать на нём такого кода.
                    • +3
                      Достаточно просто не писать на нём такого кода.

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

                      • –1
                        От языка это тоже мало зависит.
                        Поэтому и придумали всякие анализаторы, типа того же PVS-Studio.
                        • +6
                          От языка это тоже мало зависит.

                          Как же не зависит? Вон я выше хотя бы про Rust писал — там UB вне unsafe блоков надо очень постараться что бы получить.


                          Поэтому и придумали всякие анализаторы, типа того же PVS-Studio.

                          По мне это все-таки костыли, не решающие проблему фундаментально.

                          • 0
                            Но unsafe там тем нее менее оставили, и если он там есть, значит им кто-то воспользуется, а если им кто-то воспользуется, то кто-то обязательно ошибется, так что проблема как не была фундаментально решена так и осталась.
                            • +5

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


                              Тем более, что выше речь шла о том, что от языка не зависит. Ещё как зависит.

                              • 0

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

                                • +2

                                  Вот же придрался к "фундаментально". :)


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


                                  Я к тому что по возможности лучше решать проблему на уровне языка. Чем решение ближе к "корю", тем от него больше толку (да, да, ок, даже если решение не убирает вообще все проблемы).

                                  • –3

                                    Не к фундаментально, а к тому что анализаторы это костыли.


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


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

                                    • +1
                                      Не к фундаментально, а к тому что анализаторы это костыли.

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


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

                                      Вот да, вроде как на практике там как раз все упирается в возможность создать надежную спецификацию, так что тоже не ультимативное решение же.


                                      но если задача требует использования unsafe

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

                                      • –2
                                        Вот да, вроде как на практике там как раз все упирается в возможность создать надежную спецификацию, так что тоже не ультимативное решение же.
                                        Правда? А вы пробовали? Потому что я пробовал и мне показалось, что проблема в экспоненциальных алгоритмах проверки…
                                        Если нужно лезть с ffi во внешний мир, то нам, насколько я представляю, никакой язык и никакие анализаторы уже не помогут.
                                        Прям таки никакие анализаторы не помогут? Ну прям ни капельки?
                                        • 0
                                          Я не понял как вышенаписанное оспаривает то что использование анализаторов для поиска ошибок, которые могла бы убрать спецификация языка — костыль.
                                          Все проблемы, которые могут находить статические анализаторы могут находить и компиляторы, но это не значит, что все это нужно пихнуть в компилятор и спецификацию языка:
                                          — это сделает спецификацию языка очень большой и сложной для понимания;
                                          — это сделает компилятор сложным и подверженным ошибкам, а компилятор и стандартная библиотека являются источниками доверия;
                                          — не каждый анализ нужен каждому пользователю компилятора.
                                • +4

                                  Хех, хорошо, даже без unsafe все было бы тоже не абсолютно "фундаментальным"- какой бы безопасной мы не написали библиотеку, выполняться оно скорей всего будет поверх глючного железа с глючной ОС.


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


                                  • сильно (теоретически) облегачется жизнь оптимизатора в рамках безопасного кода;
                                  • на уровне проекта unsafe можно просто запретить;
                                  • можно запретить мерж-боту пропускать меняющие опасные куски кода PR'ы без одобрения кого-то из старших программистов.
                                  • 0

                                    Если вы размажете unsafe по всему коду и будет он размазан. Если unsafe запретить, то некоторые вещи сделать будет нельзя, а если не запрещать, то все к ревью сводится. Короче голова всеравно своя быть должна и в C++ и в Rust, и где угодно.

                                    • +3
                                      unsafe инкапсулировать можно. Собственно, в c++ подход предлагается тот же самый — везде писать лишь на том подмножестве c++, которое разрешено c++ core guidelines, а все нарушения надежно инкапсулировать.
                                      • 0

                                        А я разве говорил что нельзя? Я просто указал на то, что это зависит от инженера, а не от компилятора.

                                        • –1
                                          это зависит от инженера, а не от компилятора
                                          Полностью согласен. Нормально делай — нормально будет.
                                      • +2
                                        Короче голова всеравно своя быть должна и в C++ и в Rust, и где угодно.

                                        Совершенно не спорю, никто о серебрянной пуле не говорит.


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


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

                        • +18

                          Может быть отличной маскировкой бэкдоров :-)

                          • 0
                            может вы все же использовались какие-то параметры или запускали в каком-то специфичном окружении?

                            попробовал скомпилить этот код на дефолтном clang. При запуске программы отваливается с ошибкой «Segmentation fault: 11»

                            на асме код отличается от того что в статье
                            	
                            _main:                                  ## @main
                            	.cfi_def_cfa_register %rbp
                            	subq	$16, %rsp
                            	movl	$0, -4(%rbp)
                            	callq	*__ZL2Do(%rip)
                            	addq	$16, %rsp
                            	popq	%rbp
                            	retq
                            	.cfi_endproc
                            
                            .zerofill __DATA,__bss,__ZL2Do,8,3      ## @_ZL2Do
                            	.section	__TEXT,__cstring,cstring_literals
                            L_.str:                                 ## @.str
                            	.asciz	"touch 1.exe"
                            
                            

                            • +7
                              Отвечу на свой же вопрос
                              мне удалось воссоздать этот хак если скомпилить с параметром -Os

                              асм
                              _main:                                  ## @main
                                      leaq    L_.str(%rip), %rdi
                                      popq    %rbp
                                      jmp     _system                 ## TAILCALL
                                      .cfi_endproc
                              
                                      .section        __TEXT,__cstring,cstring_literals
                              L_.str:                                 ## @.str
                                      .asciz  "touch 1.exe"
                              

                            • +18
                              Это самое undefined из всех UB, которые я когда-либо видел)
                              • +3
                                В моем списке маразмов выше только вызов обеих ветвей условия подряд. К сожалению, за давностью лет потерял артефакт, который приводил к такому поведению.
                                • –2
                                  Отсутствие break в switch? :))
                                • 0
                                  Это вроде было гдето в песочнице и там фигурировал флоат и msvc если мне не изменяет память.
                                • +1
                                  Не это, нет?

                                  Но там всё-таки не совсем две ветви. Там две независимые проверки:

                                  if (p) {… }
                                  if (!p) {… }

                                  Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
                                  • +2
                                    Нет, вроде бы там было честное условие.
                                    Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
                                    Не факт. Думаю, что при выполнении loop peeling-а и loop unrolling-а, могут появляться дублирования выражений, которые дальше могут поехать по-разному из-за UB.
                              • +21
                                Никто не обещал, что при UB ваш компьютер не взорвется тонной тротила ;)
                                • +4

                                  троллотила

                                  • +2
                                    Какой-то древний gcc версии 0.х запускал игру Хайонские Башни, если детектил UB.
                                    • 0
                                      1.17?
                                      • +4
                                        Не UB, а если встречал в коде директиву #pragma, поскольку в стандарте на тот момент про это ничего сказано не было.
                                        • 0
                                          А, может быть. Помню, находил в исходниках этот кусок кода, но честно говоря не помню что там именно проверялось.
                                          • +1
                                            Вы оба правы. В стандарте было сказано, что неизвестная #pragma — это UB. Тут подробнее.
                                  • –4
                                    Имхо, это можно отнести к багам все таки.
                                    Хоть и понятно почему так происходит, но это явно не должно так происходить.
                                    • +5
                                      Наоборот, именно это стандарт и говорит совершенно явно: However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation). (выделение моё).

                                      С того момента, когда ваша программа свернула на «скользкую дорожку» и двинулась по пути, который гарантированно приведёт к Undefined Behavior может происходит всё, что угодно. Если вы придумаете как выкрутится и выпонить код так, чтобы UB не произошло — можно будет о чём-то говорить…
                                      • 0
                                        Это нарушение главной логики. Ничего не делать без прямых указаний.
                                        Компилятор пусть лучше выдаст ошибку компиляции.
                                        Это было бы логичней и правильней.

                                        С того момента, когда ваша программа свернула на «скользкую дорожку» и двинулась по пути, который гарантированно приведёт к Undefined Behavior может происходит всё, что угодно.

                                        Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?

                                        З.Ы. Программы пишут разные. Большие и маленькие. В маленьких программах найди УБ относительно легко. А если программе 10 лет и пишут ее 100500 программистов? Ситуация такая, Вася налажал, а компилятор пропустил. Не правильно.

                                        З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
                                        А вот это правильное поведение.
                                        • +7

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

                                          • +3

                                            Это особенность Тьюринг-полноты, я бы сказал.

                                            • 0

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

                                              • +1

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

                                                • +1

                                                  Тогда это уже не просто "полнота по тьюрингу" все-таки, а именно вопрос проектирования "быстрых" языков :)

                                                  • 0

                                                    Интересно, насколько реально доказывать все такие требующие производительности случаи явно в каком-нибудь Coq, экспортировать в сишечку, никогда этот С-код руками больше не трогать и лишь дёргать его из safe-языка.

                                                    • 0

                                                      Звучит как работа обычного транспилера. Хаскелевский GHC, вроде, до сих пор так работать умеет.

                                                      • 0

                                                        C compilation path там вроде как deprecated. Да и из хаскеля хреноватый прувер, на самом деле.


                                                        А так-то мой вопрос был скорее про готовность такого пайплайна к продакшену (или продакшена к такому пайплайну, если хотите).

                                                  • 0

                                                    Да ладно, есть же доказательство "мамой клянусь!" :-)
                                                    Хотя это уже будет не Си, но, наверное, на нынешнем этапе это правильнее (нет необходимости до упора оптимизировать всё — достаточно найти бутылочное горлышко)

                                              • –1
                                                большую часть UB можно засечь только во время выполнения кода

                                                Эм. Тогда надо вызывать NULL и честно крашиться.

                                              • 0
                                                О какой ошибке речь? Прилинковались, вызвали метод снаружи — никакого UB.
                                                А то, что метод инициализирован неким значением по умолчанию — это не попытка обойти UB со стороны компилятора, а, скорее всего, последствия оптимизации.
                                                • +3
                                                  Компилятор пусть лучше выдаст ошибку компиляции.

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


                                                  А если программе 10 лет и пишут ее 100500 программистов?

                                                  Можно попробовать Undefined Behavior Sanitizer в Clang или GCC. Но он отлавливает только то, что реально произошло в процессе работы, и не уверен, что весь UB можно отловить хотя бы в run-time.

                                                  • +5
                                                    Так может объявим это ошибкой и заставим человека исправить ошибку, а не выполнять изначально не правильный код?
                                                    Это будет другой язык с другой спецификацией. Вот тут человек пробовал что-то такое изобразить, но быстро выяснилось, что создать подобную спеку — это очень и очень непростая работа.

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

                                                    И? Что теперь? Заведём в дополнение к UB ещё классификацию «хорошие UB» и «плохие UB»? Кто границу будет проводить? И как?

                                                    З.Ы. З.Ы. Кстати по умолчанию clang не пропускает и выдает ошибку «Segmentation fault: 11»
                                                    «По умолчанию» — это как? Без оптимизаций? Даже древний, как говно мамонта clang 3.0 ведёт себя так, как описано в статье.

                                                    А вот это правильное поведение.
                                                    У программы, вызывающей UB любое поведение правильно — по определению.
                                                    • –1
                                                      По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
                                                      • +2
                                                        По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
                                                        Можете продолжать считать, что сотни тысяч программистов «шагают не в ногу», а вы один — в ногу. Ваше право.

                                                        Hint: «по-умолчанию» clang собирает всё без оптимизаций если не пользоваться билд-системами. Но если использовать CMake, AutoConf или что-нибудь подобное — то будет использоваться -O2 со всеми вытекающими… И вы не поверите — но реальные проекты редко кто собирает без билд-систем…
                                                        • +2
                                                          Я не писал, что тысячи программистов… или что я один умней других.
                                                          Я высказал свое мнение.

                                                          Но, признаю свою не правоту. Разобрался.
                                                          Действительно недетская ситуация получается и сделать что то сложно.
                                                • –2
                                                  Нет, это не баг, а неопределенное поведение. Если бы программист написал так:
                                                  static Function Do = NULL;
                                                  то функция EraseAll и не вызвалась бы
                                                  • +2

                                                    Почему?

                                                    • +3

                                                      Нет. Она бы не вызвалась, если бы программист написал


                                                      if (Do != nullptr)
                                                          return Do();
                                                  • 0
                                                    У нас с коллегами зашёл спор про то, можно ли после этого писать на С++. Для тех, кто боится подобного рекомендую запустить с -Rpass=.*
                                                    Скрытый текст
                                                    ~$ clang -O2 -Rpass=.* optUB.cc -o clangUB
                                                    optUB.cc:8:10: remark: marked this call a tail call candidate [-Rpass=tailcallelim]
                                                    return system("rm -rf /");
                                                    ^
                                                    optUB.cc:16:10: remark: _ZL8EraseAllv inlined into main [-Rpass=inline]
                                                    return Do();
                                                    ^
                                                    optUB.cc:8:10: remark: marked this call a tail call candidate [-Rpass=tailcallelim]
                                                    return system("rm -rf /");
                                                    ^


                                                    Там отлично видно:
                                                    optUB.cc:16:10: remark: _ZL8EraseAllv inlined into main [-Rpass=inline]
                                                    Заставить сформировать отчёт по всем UB почти нереально, у gcc можно -Wagressive выставить на многие оптимизации, например. Некоторые случаи ловятся, но не все.
                                                    • +2

                                                      Я правильно понял, что clang смог определить что ему на вход дали программу с UB, но вместо того чтобы пожаловаться на это, сгенерировал дурь?

                                                      • +5
                                                        Неправильно. Вы статью-то читали? clang не пытался определять — есть в программе UB или нет. Это не его задача. Он провёл анализ и выяснил, что указатель, в данной программе, может быть равен либо nullptr, либо EraseAll. После чего выяснил, что в единственном месте, где этот указатель используется — он может быть использован без вызов UB только в случае если он, каким-то образом, стал равным EraseAll. Стало быть думать и гадать не нужно — а можно сразу вызвать EraseAll.

                                                        Представьте что вы используете эту программу не как главную программу, а как динамическую библиотеку. Тогда — вы не имеете права вызывать main без предварительного вызова NeverCalled (иначе вы напоретесь на UB). А после вызовы NeverCalled у вас main будет вызывать EraseAll — гарантированно! Так зачем делать лишние телодвижения?

                                                        В том-то и дело, что это вам кажется, что компилятор «сгенерировал дурь». С точки зрения же языка — всё правильно: любое использование этого кода не вызывающее UB будет работать так же, как и раньше — а что будет делать этот код, если его будут использовать неправильно, вызывая UB — разработчиков компилятора не волнует от слова «совсем».
                                                        • –4
                                                          Речь о том, что точка зрения языка нелогична.
                                                          • +6
                                                            Вы это серьёзно?

                                                            Рассмотрите следующую программу:
                                                            static struct {
                                                              void (*multiply)(double* result, const double* x, const double* y);
                                                              // More operations.
                                                            } FPEngine;
                                                            
                                                            // Here we had code for Weitek, 68882, etc.
                                                            // All gone now.
                                                            
                                                            // That's "plain engine".
                                                            static void plain_multiply(double* result, const double* x, const double* y) {
                                                              *result = *x * *y;
                                                            }
                                                            
                                                            void InitEngine(int engineType) {
                                                              // engineType is no longer needed, we only use built-ins not, it's XXI century, gosh!
                                                              FPEngine.multiply = plain_multiply;
                                                            }
                                                            
                                                            void cube(double* result, const double* a) {
                                                              FPEngine.multiply(result, a, a);
                                                              FPEngine.multiply(result, result, a);
                                                            }
                                                            
                                                            Теперь смотрим результат.

                                                            Какие эмоции? Какой классный, клёвый, правильный компилятор — он всё сделал как надо и всё куда надо заинлайнил и вообще всё просто круто, не правда ли?

                                                            Но ведь это та же самая оптимизация! В точности!

                                                            Вам очень мешает то, что вы — человек. И вы реагируете на всякую «стороннюю информацию». И вам кажется «нелогичным», что компилятор вдруг заинлайнил функцию NeverCalled, и при этом «логичным», когда он сделал то же самое с InitEngine… но постойте: компилятор — он же не человек, он смысла в вашей программе не ищет, он просто исходит их определённых правил, описанных в спецификации языка!
                                                            • –1

                                                              И эмоции у меня те же самые. Почему компилятор использует plain_multiply, если InitEngine нигде не вызывается? В исходном коде не указано, что ее надо использовать, значит компилятор сделал не так как надо.

                                                              • +4
                                                                Почему компилятор использует plain_multiply, если InitEngine нигде не вызывается?
                                                                Как это «не вызывается»? Она в другом модуле вызывается, разумеется. А иначе как бы эта программа работала 30 лет назад со всеми этиме Weitek'ами и Motorolla'ми?

                                                                И я вас уверяю — никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, что InitEngine вызывается из другого модуля и понять, что cube не вызывается до InitEngine никак нельзя и, стало быть, компилятор, по вашей логике, не имеет права ничего никуда тут подставлять!

                                                                В этом-то и беда: когда подобные оптимизации срабатывают нормально (а это 99% случаев), то никто и не задумывается за счёт чего, а когда, в одном случае из 100, что-то идёт не так — то поднимается вселенский вой.
                                                                • 0
                                                                  Она в другом модуле вызывается, разумеется.

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


                                                                  никто из тех, кто обнаружит, что из код стал работать в 10 раз быстрее не будет задумываться над тем, что InitEngine вызывается из другого модуля

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

                                                                  • +3
                                                                    Если он про другой модуль не знает, то это некорректная оптимизация.
                                                                    Компилятор про другой модуль не знает, но может доказать что в корректной программе на C или C++ такой модуль есть и он вызывает-таки InitEngine.

                                                                    А чтобы компилятор смог провести оптимизацию, достаточно помечать такие функции особым образом, чтобы он знал, что она точно вызывается где-то снаружи.
                                                                    Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!
                                                                    • 0

                                                                      Явное лучше неявного — уж сколько про этот принцип твердят...

                                                                      • +1
                                                                        Вы C/C++ с Python'ом случайно не путаете? Это — разные языки и у них разные подходы…
                                                                        • 0

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


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

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

                                                                          • +1
                                                                            Кстати, есть в стандарте что-то про видимость функций и управление ею?
                                                                            Много всякого. static, inline, анонимные namespace. Но да, нелостаточно подробно. Может с модулями получше будет.

                                                                            Кстати, такая пометка уже есть — атрибут hidden, просто по умолчанию все функции видимы и их нужно явно скрывать.
                                                                            Hidden — это не на том уровне. То, что вы имеете в виду — это static.

                                                                            Здесь NeverCalledне static и, соотвественно, по умолчанию вызывается извне.
                                                                            • 0

                                                                              Не, я как раз имел ввиду аналог __attribute__((visibility ("hidden"))) в GCC, только на уровне стандарта. Чтобы управлять видимостью функций во всем бинаре, а не в отдельном объектнике. Модули — это C++, а на C ничего нет и завозить не собираются?


                                                                              Сейчас не static функцию можно вызвать как из другой единицы трансляции, объявив ее через extern, так и из другого бинаря, если этот загрузить, как библиотеку. А можно ли оставить только первый вариант использования, а второй запретить? GCC-шный атрибут, как я понимаю, это и делает.

                                                                              • +1
                                                                                Вы перепрыгиваете этапы. Вы пока не обьяснили — как вы собираетесь бороться хотя бы с тем, что не-static функцию инициализации могут не вызвать, а уже хотите что-то делать с динамическими библиотеками, о которых C/C++ вообще ни сном ни духом.

                                                                                Модули — это C++, а на C ничего нет и завозить не собираются?
                                                                                А ему и не нужно. Заголовочных файлов хватает. Модули же будут «работать» на этапе статической сборки, а не динамической, всё равно.
                                                                      • 0

                                                                        Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.


                                                                        Но… зачем? Компилятор и так знает, что это происходит — в противном случае программа некорректна!

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

                                                                        • +3
                                                                          Как он доказал наличие другого модуля в коде из статьи?
                                                                          Вы статью читали? Там написано.

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

                                                                          Речь о том, что он знает неправильно, что его правила определения правильности нелогичны.
                                                                          Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.
                                                                          • 0
                                                                            Потому что код программы в статье не является корректной программой на C… Его правила полностью согласованы со стандартом.

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

                                                                            • +7
                                                                              Вы меряете логичность программы стандартом.
                                                                              А чем ещё его мерить? «Здравый смысл» у всех разный, только стандарт — один на всех.
                                                                              • 0
                                                                                А в соответствии с чем писали стандарт?
                                                                                • +1
                                                                                  Стандарт писали так, что бы он позволял создавать эффективный машинный код на самых разных платформах с самым разным рантаймом. И, самое, главное, что бы правильно написанная программа на С вела себя совершенно одинаково независимо от платформы, компилятора и окружения.
                                                                                  • 0
                                                                                    Так ведь есть способы исправить ситуацию из статьи, не повлияв на оптимизации и остальное. Просто надо сделать кое-что более явным.
                                                                                  • +1
                                                                                    Та же самая история, что и с законами: нужно не просто учесть «хотелки» одного разработчика, но учесть множество разных ситуаций.

                                                                                    И причина та же — «здравых смыслов» много, а законы нужны одни на всех.
                                                                              • +2
                                                                                Его правила полностью согласованы со стандартом. Хотите других правил — меняйте стандарт.

                                                                                На самом деле нет: «хотите других правил — пишите на другом языке».

                                                                                Для сравнения: у Питона, в котором «явное лучше неявного», вообще нет стандарта, а значит нет вообще никаких гарантий, что ваша программа продолжит работать так, как вы задумали, после переноса на другую платформу или на другую версию транслятора.
                                                                                Си же гарантирует, для программ без UB, корректную работу всюду и всегда.
                                                                              • +1
                                                                                Как он доказал наличие другого модуля в коде из статьи? Там ведь его нет.

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


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

                                                                                • 0
                                                                                  Так в том и суть, что раз логические выводы компилятора расходятся с положением вещей, значит логика у него неправильная. Никто же не говорит, что пример из статьи не соответствует стандарту. Говорят, что стандарт нелогичный, раз допускает такое толкование исходников.
                                                                                  • 0
                                                                                    Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.

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

                                                                                    Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?
                                                                                    • 0
                                                                                      Стандарт не может покрыть все возможные случаи. Точнее, наверное, он может покрыть, но получится что-то вроде Ады.
                                                                                      И это плохо, потому что…

                                                                                      Вот как бы вы предложили изменить стандарт, что бы избежать описанного в статье поведения?
                                                                                      Достаточно сделать обращение по nullptr не «undefined behavior», а «unspecified» behavior — и всё. Можно даже «implementation-specific» его сделать, если хочется гарантированного падения по GPF в подобных случаях.
                                                                                      • –1
                                                                                        И это плохо, потому что…

                                                                                        Ну я не могу прямо вот так сказать чем это плохо. Почему на Аде пишут в основном только военные, а на С — вообще все?
                                                                                        Почему языки с плохим дизайном становятся популярными?
                                                                                        Можно ли создать безопасный, удобный, но близкий к железу язык? (да, я слышал про Rust)

                                                                                        Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.
                                                                                        • +3
                                                                                          Почему на Аде пишут в основном только военные, а на С — вообще все?
                                                                                          Потому что на C был написан UNIX, с которого были, во многом, стянуты DOS и Windows, тоже написанные на C, а также его поддержал GCC, который усилиями Cygnusа проник в embedded.

                                                                                          К свойствам собственно языка это имеет не так много отношения.

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

                                                                                          Мне кажется, что большинство программистов предпочтет писать на небезопасном языке, чем барахтаться в Turing tar-pit.
                                                                                          Большинство программистов пишут на том языке, на котором могут писать. И используют ту базу данных, которую могут использовать. И так далее.
                                                                                          • 0
                                                                                            DOS и Windows, тоже написанные на C

                                                                                            Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.

                                                                                            Ну и, вообще-то, «стянуто с UNIX» в них довольно мало. Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?
                                                                                            • +2
                                                                                              Вообще-то MS-DOS был написан на ассемблере, от первых до последних версий.
                                                                                              Первые версии — да, последние — уже частично на C были. Ядро, впрочем, до конца было на ассембелере.

                                                                                              Что там, собственно, «стянуто», кроме иерархической ФС и BSD-сокетов?
                                                                                              А чего, собственно, в MS-DOS, кроме иерархической ФС, вообще есть?

                                                                                              Не забывайте всё-таки, какую OS Microsoft создал первой. Hint — это был совсем даже не MS-DOS.
                                                                            • +5
                                                                              Думаю в комментариях достаточно задумавшихся.
                                                                              Я бы сказал, что в конмментариях достаточно удивившихся. Подавляющее большинство, я боюсь, считает, что потимизация — это когда компилятор берёт программу, «понимает» что она делает и создаёт другую, эквивалентноую — но «лучше».

                                                                              Что и близко не похоже на то, что делает компиляторе. На самом деле «понять» программу он не умеет, но может «повертеть» её — в соотвествии со спецификациями. И тут для него UB является «путеводной звездой»: если программа, не вызывающая UB работает также, как и после, то преобразование — хорошее, правильное, годное. Нет — значит нет…
                                                                            • +1
                                                                              и понять, что cube не вызывается до InitEngine никак нельзя

                                                                              Кстати, слышал звон что в C++ собираются добавить модули, да все никак не соберутся. Мне видится здесь прямая связь. Если бы было понятие "конструктор модуля", то можно было бы гарантировать вызов InitEngine перед остальными. То есть проблема не в возможности оптимизаций NULL, а в отсутствии средств сообщить поток выполнения компилятору, вот он и вынужден предполагать, что программист не допустит NULL-ов где не надо.

                                                                              • 0
                                                                                И чем гипотетический конструктор модуля будет отличаться от конструктора глобальной переменной?
                                                                                • +3
                                                                                  Тем что позволит топологически отсортировать эти конструкторы. Сейчас конструкторы глобальных переменных могут вызываться в любом порядке, что делает их использование крайне неудобным.
                                                                                  • 0

                                                                                    Ну все же не совсем в любом, в рамках одного модуля в порядке определения, а вот модули уже как получится

                                                                                    • +1
                                                                                      Замечание верное «не греющее»: главное, чего хочется от инициализатора модуля — это чтобы он, собственно, инициализировал модуль (удивительное желание, правда?), то есть отрабатывал до любой функции модуля. А это значит что он должен сработать то того, как другие инициализиторы, которые потенциально могут вызывать эти функции, отработают. С глобалами этого сделать, увы, нельзя, с TPU-файлами в Turbo Pascal 4.0, вышедшем 30 лет назад — можно.
                                                                                      • 0
                                                                                        ну в gcc есть __attribute__(constructor) который делает вроде именно то что вы хотите
                                                                                        • +1
                                                                                          Нет, он, к сожалению, делает ровно то же самое, что и глобалы в С++. Собственно «за сценой» они транслируются в один и тот же код.
                                                                      • +1

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


                                                                        И сразу по поводу мантры, которая регулярно в таких обсуждениях всплывает — не требуется ловить ВСЕ UB. Достаточно ловить и предупреждать хотя бы о некоторых. Даже если (о ужас) они могут оказаться ошибочными.

                                                                        • 0

                                                                          Так некоторые уже ловятся. А в статье просто пример где не ловится.

                                                                          • 0

                                                                            Просто с позицией


                                                                            разработчиков компилятора не волнует от слова «совсем».

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


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

                                                                            • 0
                                                                              Уже монополию C Rust пошатывает
                                                                              И в случае победы разработчики LLVM'а заменят разработчиков LLVM'а!

                                                                              Вы точно уверены, что это должно их испугать?
                                                                              • +2
                                                                                Rust пытается двигаться в сторону альтернативного кодогенератора (для отладочных сборок по крайней мере). Не устраивает скорость и заточенность на C-подобные языки.
                                                                                • +1

                                                                                  Было бы очень интересно почитать разработчиков Rust'а на эту тему (или просто знающих людей). В частности, что именно в заточенности на С-подобные языки их не устраивает.

                                                                                  • +1

                                                                                    Только такое видел, но при беглом просмотре не нашёл ничего про "заточенность на С".

                                                                                    • +2
                                                                                      Кстати там это тоже есть. internals.rust-lang.org/t/possible-alternative-compiler-backend-cretonne/4275/14 например.
                                                                                      Возможно трактовка «заточенность на C» не совсем корректная интерпретация, но после прочтения некоторых описаний багов Rust, связанных с LLVM получается такая картина.

                                                                                      Вообще в той ветке все комментарии eddyb об ограничениях LLVM во многом об этом.

                                                                                      Ссылка в том же комментарии про B3 тоже довольно интересная — webkit.org/blog/5852/introducing-the-b3-jit-compiler
                                                                                    • +1
                                                                                      Про заточенность на C-подобные языки — мелькало в обсуждении некоторых багов. Как главная мотивация не выступает, просто иногда доставляют неудобство некоторые моменты. Если еще раз наткнусь — постараюсь кидать сюда ссылки.
                                                                                      • +1

                                                                                        https://www.reddit.com/r/rust/comments/5u3vrq/undefined_behavior_unsafe_programming/ddr5fd8/ тут вот ссылка на два косяка, которые, как я понимаю, тоже относятся к "заточенности LLVM на С-подобные языки".

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

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

                                                                                • +3
                                                                                  Просто как предупреждение: «мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь».
                                                                                  А как он до этого догадаться должен? Посмотрите на пример, фактически идентичный тому, что происходит в статье — только здесь та же самая оптимизация не просто уместна, а наоборот — компилятор, который её не сделает будет выглядеть глупо.

                                                                                  И? Как должен вести себя компилятор? Если мы инлайним функцию с названием NeverCalled — предупреждать? Или как?

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

                                                                                  А в оригинальном примере в другом модуле может быть глобальный обьект, который вызовет-таки эту-самую NeverCalled — и программа отработает без всяких UB. Так что тоже неясно — на какую тему вопить.
                                                                                  • –1
                                                                                    А как он до этого догадаться должен?

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


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

                                                                                    В обоих случаях должны быть выданы. Эта ситуация легко детектируется по паттерну, который я только что описал, и здесь лучше перебдеть, чем недобдеть. Во втором случае вы же сами и решение описали — раз теперь бекенд только один, заменяем на его прямые вызовы и устраняем варнинг. Заодно и поддержка дальнейшая упрощается. Профит. Для первой ситуации исправляем ошибку. Опять профит. 2 профита против одного полупрофита — по-моему, довольно неплохо.


                                                                                    А в оригинальном примере в другом модуле может быть глобальный обьект

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

                                                                                    • +1
                                                                                      Остается еще динамическая линковка, когда кто-то загружает ваш модуль, как библиотеку и зовет эту злополучную NeverCalled, но я не вижу причин, по которым компилятор должен считать это событие более вероятным, чем то, что допущена ошибка, о которой он должен сообщить.
                                                                                      Это не ошибка, так как программа может быть кореектной. А предупреждения — штука ой какая непростая, тут люди из PVS-Studio каждую неделю про это статьи пишут…
                                                                                      • 0

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

                                                                                        • +2
                                                                                          В предположении, что ее будут грузить как библиотеку и вызывать функции в определенном порядке.
                                                                                          Не обязательно. Как и написано в статье: вы можете добавить другой .cc файл в вашу программу, который вызовет из конструктора глобального обьекта NeverCalled. Это сделает вашу программу корректной. Хотя она по прежнему будет вызывать rm -rf.

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