Pull to refresh

Comments 299

За что я, среди прочего, не долюбливаю плюсы, предпочитая старый-добрый, тепло-ламповый паскаль :)
В посте приводится пример из чистого C.

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

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

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

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

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

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

Хотя, конечно, Rust-овские lifetime тоже не ягодки. В плане использования ещё не сильно сложно, а вот в плане правильного проектирования — сложно.

Небольшое уточнение, не "Rust, где практически все UB возможно только в unsafe блоках", а "Rust, где UB возможно только из-за unsafe блоков". Примером этого может служить код:


fn main() {
    let invalid_bytes = b"\xf0\x28\x8c\x28";
    let invalid_text = unsafe {
        std::str::from_utf8_unchecked(invalid_bytes)
    };
    innocently_print_text(invalid_text);
}

fn innocently_print_text(text: &str) {
    println!("{}", text);
}

Поведение innocently_print_text не определено в данном случае, хотя код не использует unsafe. Виновник — другая часть кода, которая нарушила гарантии типа str.

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

Ну, на то они и ночники. Приветствую вас в сегодняшнем дне :)
Так или иначе, рациональное зерно в словах Zakyann есть. Оптимизация — это хорошо, кто же спорит. Но если мы приходим к ситуации, когда итоговый машинный код абсолютно не соответствует исходному (спекулятивная трансформация), это ненормально. Разрабатывая программу, ты должен быть уверен, что машинный код будет делать то же самое (я не о процессе, а о результате — процесс может быть иным из-за оптимизации, как, например, сдвиг вместо умножения), что исходный код на ЯВУ. Ты не должен думать за компилятор.
Возможно данный конкретный пример — это уже перебор. Возможно в данном случае компилятор должен был бы сгенерить вызов функции по адресу NULL. Это более ожидаемое поведение с точки зрения здравого смысла. Но понимаете, очень тяжело засунуть здравый смысл в компилятор.
А с точки зрения стандарта этот UB ничем не отличается от других UB.
Но понимаете, очень тяжело засунуть здравый смысл в компилятор

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

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

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

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

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


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

Запретить вообще всем указателям-ссылкам на уровне языка быть null'ами, а там где это реально нужно — программист должен использовать всякие Option/? типы-обертки
Посмотрите на C++ Core Guidelines, пункт I.12. Собственно, к этому решению c++ и движется. Правда, обертка получается не на уровне языка, а в отдельной библиотеке, и проверка правила на уровне статического анализатора, но это уже детали.
Проблема в том, что в стандарте на ЯВУ не прописан 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);
Я говорю о результате, еще раз: если исходный текст предпписывает упасть в GPF, машинный код должен упасть в GPF.
В какой конкретно строке на какой странице стандарта описано GPF и как оно работает? Нет ничего такого в стандарте C++ — зато есть запрет на разименование nullptr. Разименовал — ССЗБ, получи «подарок».

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

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

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

if !is_valid_addr(abc)
И как, я извиняюсь, ваша, is_valid_addr работать будет? Если она будет возврашать false не только на nullptr, но и на 1 — так компилятору ещё лучше будет! Он теперь из факта разименования будет получать информацию не только о том, что оказатель не равен nullptr, но и о том, что это не 1, тоже!
UFO just landed and posted this here
В идеале, конечно, компилятор должен был бы отвергнуть такую программу на этапе компиляции
Это с какого перепугу? Добавьте в программу модуль с глобальным обьектом, конструктор которого вызовет NeverCalled — и все UB из программы пропадут, она станет корректной и будет работать так, как написано.

Узнать — есть ли в программе такой модуль компилятор никак не может, но, как уже было 100 раз повторено: если такого модуля нет, то программист — ССЗБ и заслуживает того, что получил…
UFO just landed and posted this here
Это тоже не очень строгая формулировка, но, надеюсь, доносит мысль.
Не доносит. Я могу вызывать NeverCalled, в том числе, из модуля, который я загружу через LD_PRELOAD.

Надо было бы LTO — ну, значит, с LTO.
LTO нужно специально «заказывать». И, в общем, в этот момент уже сложно выдаватаь подобные ошибки…
UFO just landed and posted this here

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


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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

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


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


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

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


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


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

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

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


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

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


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

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

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

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


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


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

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

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

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

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

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


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


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

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

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

попробовал скомпилить этот код на дефолтном 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"


Отвечу на свой же вопрос
мне удалось воссоздать этот хак если скомпилить с параметром -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"

Это самое undefined из всех UB, которые я когда-либо видел)
В моем списке маразмов выше только вызов обеих ветвей условия подряд. К сожалению, за давностью лет потерял артефакт, который приводил к такому поведению.
Не, там была именно конструкция вида if (x) { A } else { B } и при этом и А и B выполнялись (в неопределенном порядке).
Это вроде было гдето в песочнице и там фигурировал флоат и msvc если мне не изменяет память.
Не это, нет?

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

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

Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
Нет, вроде бы там было честное условие.
Не думаю, что можно сделать так, чтобы обе ветви if'а исполнялись — то что исполнится только одна ветвь определяется строением SSA-дерева.
Не факт. Думаю, что при выполнении loop peeling-а и loop unrolling-а, могут появляться дублирования выражений, которые дальше могут поехать по-разному из-за UB.
как выяснилось благодаря Meltdown, в реальности исполняются именно обе ветви, просто результат исполнения ложной ветви приходится узнавать через side channel.
Никто не обещал, что при UB ваш компьютер не взорвется тонной тротила ;)
Какой-то древний gcc версии 0.х запускал игру Хайонские Башни, если детектил UB.
Не UB, а если встречал в коде директиву #pragma, поскольку в стандарте на тот момент про это ничего сказано не было.
А, может быть. Помню, находил в исходниках этот кусок кода, но честно говоря не помню что там именно проверялось.
UFO just landed and posted this here
Наоборот, именно это стандарт и говорит совершенно явно: 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 не произошло — можно будет о чём-то говорить…
UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

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

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

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

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


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

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

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

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

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

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

А вот это правильное поведение.
У программы, вызывающей UB любое поведение правильно — по определению.
UFO just landed and posted this here
По умолчанию clang не пропускает и выдает ошибку. Имхо считаю тему закрытой.
Можете продолжать считать, что сотни тысяч программистов «шагают не в ногу», а вы один — в ногу. Ваше право.

Hint: «по-умолчанию» clang собирает всё без оптимизаций если не пользоваться билд-системами. Но если использовать CMake, AutoConf или что-нибудь подобное — то будет использоваться -O2 со всеми вытекающими… И вы не поверите — но реальные проекты редко кто собирает без билд-систем…
UFO just landed and posted this here
Нет, это не баг, а неопределенное поведение. Если бы программист написал так:
static Function Do = NULL;
то функция EraseAll и не вызвалась бы
UFO just landed and posted this here

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


if (Do != nullptr)
    return Do();
У нас с коллегами зашёл спор про то, можно ли после этого писать на С++. Для тех, кто боится подобного рекомендую запустить с -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 выставить на многие оптимизации, например. Некоторые случаи ловятся, но не все.

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

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

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

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

Рассмотрите следующую программу:
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… но постойте: компилятор — он же не человек, он смысла в вашей программе не ищет, он просто исходит их определённых правил, описанных в спецификации языка!

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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


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

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

Вы точно уверены, что это должно их испугать?
Rust пытается двигаться в сторону альтернативного кодогенератора (для отладочных сборок по крайней мере). Не устраивает скорость и заточенность на C-подобные языки.
UFO just landed and posted this here

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

Кстати там это тоже есть. 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
Про заточенность на C-подобные языки — мелькало в обсуждении некоторых багов. Как главная мотивация не выступает, просто иногда доставляют неудобство некоторые моменты. Если еще раз наткнусь — постараюсь кидать сюда ссылки.
UFO just landed and posted this here
Просто как предупреждение: «мил человек, обрати внимание, что тут я делаю то-то, может, это не совсем то, что ты хочешь».
А как он до этого догадаться должен? Посмотрите на пример, фактически идентичный тому, что происходит в статье — только здесь та же самая оптимизация не просто уместна, а наоборот — компилятор, который её не сделает будет выглядеть глупо.

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

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

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

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


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

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


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

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

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

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

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

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

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


«что мне прикажете с этим делаеть?»

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

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

Если у компилятора здесь возникают вопросы
У компилятора, как раз, никаких вопросов не возникло. Всё сделано в соотвествии со спеками и ни о чём беспокоиться не нужно — хоть ты -Wextra-extra-extra задай…
В обоих случаях должны быть выданы.
Это мне на корректный код, который даже ни по одному критерию не является ни разу «проблемным» (с точки зрения корректности, не скорости) будут предупреждения выдавать? Нифига себе.

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

Заодно и поддержка дальнейшая упрощается.
Поддержка упростится до тех пор, пока мы не захотим разные Engine'ы снова оживить. И почему вдруг корректный, роботающий код нам нужно будет переделывать чтобы компилятор «успокоить»? Не много ли он на себя берёт?

Как я уже написал выше, компилятор знает, вызывает кто-то эту функцию или нет в собираемой программе.
Ни черта компилятор не знает, уж извините. Модули опять отложили (может в C++20 будут) потому о том, есть ли в программе конструкторы вызывающие NeverCalled компилятор может только догадываться. UB здесь является путеводной звездой: если что-то позволяет нам избежать UB только одним способом — значит, в привильной программе, этот способ и будет вызван.

Ну когда захотите, тогда и вернете на место. В конце концов, вы же уже переписали код и убрали зависимость от engineType, почему остановились на этом? Для AVR у вас 2 варианта — либо там точно такая же функция используется, либо своя. Без реинкарнации разных Engine это делается препроцессором и там опять одна функция. В обоих случаях для поддерживаемости лучше записать прямо, чего вы хотите, а не полагаться на оптимизации компилятора. Или отладочную версию программы без оптимизаций вы собирать не собираетесь?


Ни черта компилятор не знает, уж извините.

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

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

Без реинкарнации разных Engine это делается препроцессором и там опять одна функция.
Не обязательно. На 80386 выбор между 80387 и 3167 выполнялся в рантайме. Выбор между MaverickCrunchем, softfp и VFP тоже может оказаться полезным выбирать в рантайме.

То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…

Разумеется, он не знает, действительно ли вызовут функцию, но знает, возможно ли это в принципе или нет.
Если фукнция эскпортирована (а NeverCalled в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…

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


Если выбор в рантайме есть — то это не та ситуация, которую мы обсуждаем.


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


Если же InitEngine таки не вызывается внутри этого бинаря, это значит одно из двух:


  • вы специально удаляли код ее вызова из вашего бинаря (помним, мы рассматриваем ситуацию, когда вызова InitEngine в бинаре нет). Что мешало пойти дальше и удалить теперь уже бесполезный косвенный вызов в cube, вы же все равно правили связанный код?
  • она вызывалась только снаружи. Что мешало теперь сделать функцию пустой, ведь от того, вызывают ее или нет, ничего не меняется, и удалить теперь уже бесполезный косвенный вызов в cube?

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


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


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


То что у вас в конкретном бинарнике есть только один вариант не означает, что никаких других из этих исходников не собирают…

Не понял, что вы хотите этим сказать.


Если фукнция эскпортирована (а NeverCalled в оригинальном примере экспортирована) — то это возможно. Потому он её и оставляет, хоть и пустую…

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

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

Такое случается сплошь и рядом.

Если внутри вашего бинаря функция настройки (InitEngine или NeverCalled) вызывается — это опять не та ситуация, мы рассматриваем ситуацию, когда она не вызывается изнутри бинаря (о вызовах снаружи далее).
В этом случае можно лишь обсуждать интересные сайд-эффекты решений, принятых для других случаев. Это неправильная программа и как она работает — никого не волнует.

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

В этом мире, на этой планете у компилятора нет информации о том — будет вызвана функция InitEngine/NeverCalled или не будет. Просто нет — и всё. Однако есть информация о том, что её можно вызвать (этим заведует аттрибут static). Из чего он и исходит.

Но когда он не знает, вызывали функцию или нет, он должен отреагировать на это предупреждением.
Никогда не знает. Так C/C++ устроен. Странно, что для вас это — открытие. Обо всех подобных преобразованиях вопить? Зачем? Там процент ложных срабатываний будет 99%! Кому это нужно?

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

А потом — дурак Вася делает так:
$ clang -Os module.cc -o module.o
$ cat > helper.cc
int something = 0;
^D
$ clang -Os helper.cc -o helper.o
$ clang module.o helper.o -o program

А умный Петя делает так:
$ clang -Os module.cc -o module.o
$ cat > helper.cc
void NeverCalled();

static struct Helper {
Helper() {
NeverCalled();
};
} helper;
^D
$ clang -Os helper.cc -o helper.o
$ clang module.o helper.o -o program

Так вот если я правильно понимаю вашу идею, то сверхтрансцендентный мегамозг^H^H^H^H оптимизирующий компилятор должен предсказать будущее и в момент компиляции файла module.cc угадать — кто и что потом допишет в соответствующей программе!

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

Изнутри получившегося бинаря она не вызывается, о чем компилятору точно известно (после линковки)
Стоп. Тпру. Приехали. Линкер — это линкер. Он не имеет представления не только о функциях, но и о C++ вообще (за исключением mangling'а имён и то это не обязательно). В момент, когда происходит линковка уже некому выдавать сообщения об ошибках! Вся информация о том, что там происходит в исходном коде безвозвратно потеряна ещё на этапе компиляции. Есть, правда, DWARF, но уж оттуда что-то выуживать — точно не задача линкера.

Хотите получать сообщения на этом этапе — пишите отдельную утилиту. А лучше используйте какой-нибудь PVS-Studio/Coverity/etc.
А где вы видите противоречие?

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


Так вот если я правильно понимаю вашу идею, то сверхтрансцендентный мегамозг^H^H^H^H оптимизирующий компилятор должен предсказать будущее и в момент компиляции файла module.cc угадать — кто и что потом допишет в соответствующей программе!

Да почему в процессе компиляции файла module.cc-то??? Я вам уже сколько сообщений подряд твержу, даже в скобках детально пояснять все стал. Не в процессе компиляции, а в процессе линковки всех объектников программы! На строчках


$ clang module.o helper.o -o program

Есть, есть тут информация о том, вызывается функция NeverCalled или нет. Как код вашего умного Пети будет работать, если компилятор даже понятия не имеет, вызывается ли функция NeverCalled или нет!???


В момент, когда происходит линковка уже некому выдавать сообщения об ошибках!

Ну елки-палки. Не ошибки, а предупреждения. И код переписывать никто не просит. И что значит некому? А undefined symbol кто выдает? Чем это отличается от unused symbol?


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

Противоречие в том, что странные преобразования, приводящие к неочевидным багам, компилятор делает для оптимизации, а рядом вы забиваете на нее большой болт и пытаетесь в рантайме выбрать из одного варианта. Зачем?
Не «зачем», а «почему». Потому что мне так удобнее. Если код нужно будет исполнять на системах, где возможны несколько несовместимых сопроцессоров (да хотя бы выбирать между SSE4.2 и AVX'ом), то это может быть полезно переключать в рантайме. А если мы тот же код компилируем под одну платформу (скажем прошивка под конкретную железку) — то компилятор всё заинлайнит и все довольны.

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

Да почему в процессе компиляции файла module.cc-то???
Потому что там и только там используется компилятор C/C++.

Я вам уже сколько сообщений подряд твержу, даже в скобках детально пояснять все стал. Не в процессе компиляции, а в процессе линковки всех объектников программы!
В процессе линковки всех объектников компилятор не участвует. Этим занимается совсем другая программа — линкер. О C++ она знает чуть более, чем ничего.

Как код вашего умного Пети будет работать, если компилятор даже понятия не имеет, вызывается ли функция NeverCalled или нет!???
В соответствии со стандартом, очевидно.

А undefined symbol кто выдает? Чем это отличается от unused symbol?
Тем, что undefined symbol — это ошибка, а unused symbol — нормальная ситуация. В типичной программе — сотни, тысячи, десятки тысяч unused symbol'ов! Откуда они берутся? Да очень просто. Простейший пример:
int square(int x) {
  return x * x;
}
int cube(int x) {
  return square(x) * x;
}
Прличный компилятор почти наверняка вставит square в cube — но удалить её он не может, так как её кто-нибудь может «снаружи» позвать. Или даже любой класс: ABI C++ так устроен, что каждый конструктор должен присутстьвовать в двух ипостасях — «финальный» и «нефинальный». Если у класса нет потомков — то «нефинальный» конструктор никто вызывать не будет. И ещё десятки других вариантов.

Линкер с опцией --gc-sections всё это безобразие может убрать, но выдавать предупреждения по этому поводу — это бред.

а как известно, линкер и компилятор — это уже давно один и тот же исполняемый файл, нет уже между этими словами такой границы, какая была ранее
Чего? Вы с ума сошли или где? Линкеры — живут тут и тут, компиляторы — тут и тут. Мужду ними не просто «границы» — это вообще разные проекты, выпускаемые разными людьми в разные моменты времени.

Если вы даже этого не понимаете — то о чём вы тут вообще говорите?

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

Если у вас будет выдано 10'000 сообщений из-за того, что всевозможные за'inline'ные функции не используются — то никому от этого хорошо не будет. А чтобы грамотную диагностику сделать с учётом наличия многих компиляторов и многих линкеров, а также инлайнинга и автоматически создаваемых функций (всякие typeinfo) — это нужно не один человеко-год в это вбухать. И ради чего? Чтобы выдать сообщение, которое человек, считающий, что обращение к nullptr обязательно порождает GPF даже читать не будет?

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

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

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

Сегодня среды разработки предоставляют такую кнопку, не просто с одной программой для линковки и компиляции, а с одной подпрограммой. Но при этом всё равно не делают такого дальнодействующего анализа, хотя у такой подпрограммы явно уже достаточно данных для выводов относительно того как должна работать функция, и какой адрес она должна разименовать.
Ага, а комитет по стандартизации строительных норм должен строить дома, разработчики стандартов продуктов — производить продукты.
Вы проигноровали суть моего сообщения. Линкер + компилятор вместе Обладают полной информацией про намерения программиста и про платформу назначения. И, соответственно, должны использовать эту информацию при построении объектных файлов и экзешника.

Легко сделать стандарт «должна быть кнопка „сделать красиво“, которая делает красиво». Сложно сделать пакет инструментов, который будет такому стандарту соответствовать, оставаясь удобным для пользователей, и понятным всем, знакомым с логикой и базовым синтаксисом.
«Линкер + компилятор вместе Обладают полной информацией про намерения программиста и про платформу назначения»

В каком-то идеальном мире, где сферические кони в вакууме летают. А на практике ПОЛНОЙ информацией про намерения программиста даже программист не обладает.

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

Знает он только если всё собирается с lto если собираются отдельные модули то ничего неизвестно

Это почему это? В каждом собираемом объектнике наружу торчат "порты", в которые нужно подключить "порты" других объектников, что и делается на этапе линковки. Если какие-то порты оказались незаполненными — это ошибка линковки — undefined symbol называется. Вот если мы заюзали NeverCalled или InitEngine в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.

Вот если мы заюзали NeverCalled или InitEngine в одном из таких портов, это автоматически означает, что они используются, а если нет — автоматически, что не используются.
Серьёзно? Берём модуль из статьи, добавляем следующее (в другом файле):
void NeverCalled();

void SomeoneIsAnIdiotBwahaha() {
  NeverCalled();
}
И вуаля: согласно вашему критерию — мы «заюзали» NeverCalled, все «порты» «запортили» и вообще всё стало ништяк. Вот только неопределённое поведение никуда не делось…

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


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

То, что это вторая функция не вызывается — уже другая проблема.
Нет — это та же самая проблема. Вы жалуетесь, что компилятор порождает код, который предполагает что определённая функция будет вызвана — но вы не можете определелить — будет ли она вызвана на самом деле! В конце-концов там может меню быть и инструкция для оператора «перед тем, как запускать вычисления зайдите в меня „выбор FPU“ и укажите правильный сопроцессор, в противном случае программа может не работать». И как вы это всё будете отслеживать в вашем линкере?

Компилятор граф вызовов строить может.
Он не может даже понять — вызовется ли та или иная функция или нет. проблема остановки — и это если ещё оператор не задействован! В противном случае вопрос провоцирования UB и вовсе нерешаем…

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

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

Напомню, вопрос не в том, вызовется NeverCalled() или нет — вопрос в том, вызовется ли она до вызова Do().
Удаление мёртвого кода тут вообще никаким боком.
Я внимательно читал статью, но плохо прочитал комментарий на который ответил, извините.

С UB дело такое. Да, когда он случается, компилятор (и рантайм) имеют право делать что угодно. Но во всём многообразии этого «что угодно» есть более лучшие варианты поведения чем другие. Было бы хорошо если бы у компилятора был флажок «не делай UB-elimination». Потому что, как вы правильно говорите, переменная может быть либо nullptr либо EraseAll. И компилятор не способен доказать что она никогда и ни за что не будет nullptr. Таким образом мы избавляемся от UB в compile-time (наш код компилируется ожидаемым, предсказуемым образом) и переносим его в run-time. А уже в run-time для нашей конкретной платформы вызов функции по нулевому указателю имеет вполне конкретное поведение — шлёпнуться по SIGSEGV. В момент компиляции компилятор уже вполне в курсе target-платформы и уже имеет право знать как на ней обрабатываются вызовы функций по nullptr.

С точки зрения спецификации, ничего не изменилось — как был UB, так UB и остался (ибо спецификация ничего не говорит об SIGSEGV). Зато фактическое поведение перестало вызывать WTF и диагностируется вполне штатными механизмами. В общем, principle of least astonishment рулез.
С точки зрения спецификации, ничего не изменилось — как был UB, так UB и остался (ибо спецификация ничего не говорит об SIGSEGV). Зато фактическое поведение перестало вызывать WTF и диагностируется вполне штатными механизмами. В общем, principle of least astonishment рулез.
А вот принципы «10x тормоза» — это не рулез ни разу.

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

Извините, но нет. В момент компиляции ничего не знает про рантайм и настройки процессора. Я вполне могу замапить по нулевому адресу страничку памяти и у меня не будет никаких SIGSEGV. Мой рантайм (например bare metal прошивка) может ничего не знать про SIGSEGV.
Вы не забывайте что C — это не только x86. И что его рантайм — это не только «богатая» ОС.
Вот из-за таких вот сюрпризов у меня «непонятно почему правильно работающая программа» вызывает на порядок больше паники, чем «непонятно почему неправильно работающая программа»
UFO just landed and posted this here
Кстати да, очень жёсткий пример для публичного кода. Можно было ограничиться prinf(«FAIL»); Я вот сразу пошёл компилить и проверять. Хорошо хоть clang'ом скомпиленное не запускал. Из-под рута не сижу, но приятного мало и для юзера.
А чем выполнение под рутом лучше выполнения под юзером?
Насколько я понимаю — снесется всё до чего дотянется rm, в том числе и home/Мне вот ОС не ценна. Переставлю за час.
А вот потеря пользовательских данных — уже более критична. Хоть большая часть и бэкапится — разгребать последствия придется всё равно…
Да без проблем. Нет в Windows rm -f
Вроде как сейчас в большинстве дистрибутивов эта команда не выполнится.
Нужно писать rm -rf --no-preserve-root /
Проверять это я конечно не буду :)

Надо ремарочку поставить. Все трюки выполены професионалами, не потвторяйте это дома. )))

на работе можно )))

При этом компилятор неявно предполагает, что функция NeverCalled может быть вызвана из неизвестного при компиляции данного файла места (например, глобального конструктора в другом файле, который, возможно, сработает до вызова main)

А почему он не предполагает, что там же может присваиваться другое значение переменной Do?

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

Вот тут явная логическая ошибка. Никто не «допускает» в своей программе неопределённое поведение. Программисты пишут код, программист никогда не пишет «неопределенное поведение начинается здесь». В этом коде может быть, а может не быть неопределенного поведения. Код, содержащий неопределенное поведение ничем не отличается от кода, не содержащего его.


Кто-то конечно может считать, что «настоящий программист» должен помнить наизусть всю спецификацию языка, с которым работает. Проблема в том, что эти 15 человек на планете (попадающие под определение «настоящего программиста») физически не смогут написать весь код, который необходим всему остальному человечеству.


Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.

Эм. Я уверен это было в значении "допустил ошибку", только "ошибка" конкретная — UB.

О том и речь:
Конечно, игра «напиши еще одну строчку кода и не отстрели себе голову» — очень увлекательная логическая головоломка. Но я хоть убей не понимаю, как ей пользоваться для написания настоящих приложений в 2017 году.

А, ну если это было не в смысле "компилятор виноват", а "язык язык", то я целиком согласен и выход простой — использовать другие языки, где меньше UB. Или высокоуровневые (когда задача позволяет), или более хитрые низкоуровневые ;) .

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

Компилятор не заметит и не исправит ошибку на единицу, не заменит неправильное "/2" на "*2" (из-за которого, скажем, будет просматриваться только четверть массива вместо полного) и так далее.

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

Потому что здесь нет ошибки программиста.

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


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

Потому что здесь нет ошибки программиста.
Серьёзно? А тут:
int *p = new int[100];
for (int i=0; i<1000000000; i++)
  p[i]=i;
Тоже всё зашибись? А тут:
int *p = new int[1000000];
delete[] p;
for (int i=0; i<1000000000; i++)
  p[i]=i;
Тоже всё распрекрасно? Или вот так:
int* foo() {
  int arr[100];
  for (int i=0;i<100;i++) {
    arr[i] = i;
  }
  return arr;
}


Нет, не ндравится? Не хотите заставить компилятор добиваться, чтобы такие программы работали? А в чём отличия с примером из статьи?

Отличие в том, что в примере из статьи программистом не указан вызов функции, а в ваших примерах указано обращение к массиву.

Ну то есть вы всё-таки предлагаем делить UB на «хорошие» и «плохие». Ok.

Что будем с подобным примером делать? Оптимизировать или мириться с тем, что какой-нибудь ICC будет наш компилятор рвать «как тузик грелку» в некоторых тестах и некоторых программах?
Нет, это было к вопросу о том, почему в статье нет ошибки программиста, а у вас есть. Там не написано, а компилятор предположил, а тут написано явно. И да, это разные виды UB, и вполне можно обрабатывать их по-разному.

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

А чем вам не устраивает подход "явно обозначать, что функция не вызывается извне"?

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

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


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

Хочу от компилятора ошибку в таком случае. Как и в случае с UB
Он компилятора? То есть он должен проанализовать ваш код и понять, что он, вдруг, где-то в рантайме чего-то переполнит?

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

А надо было анализировать так: там либо NULL, либо EraseAll. При этом, там EraseAll никогда не присваивается, т.к. Do() никогда не вызывается. Значит, там всегда NULL. По стандарту там нулла быть не должно. Ошибка компиляции, стоп машина.

Что не так с этой логикой?
там либо NULL, либо EraseAll. При этом, там EraseAll никогда не присваивается, т.к. Do() никогда не вызывается.
Нет, не так. там либо NULL, либо EraseAll. При этом EraseAll присваивается в Do(), которая может быть вызвана извне, а писать программу, в которой в этом месте NULL, программисту запрещено.

Уверены, что там всегда NULL? =)
Тогда нужно явно помечать типа #pragma дергается снаружи, а по-умолчанию считать, что вызова нет. По крайней мере в тех ЯП на которых я пишу дизайн языка предусматривает именно то. Не компилятор думает «ну раз там нулла по спеке быть не может, значит вызывается этот метод», а требует явной аннотации «мамой клянус», если программист так уж уверен.
Тогда нужно явно помечать типа #pragma дергается снаружи, а по-умолчанию считать, что вызова нет. По крайней мере в тех ЯП на которых я пишу дизайн языка предусматривает именно то.
А в C++ — наоборот. Если функция не дергается снаружи, она помечается static, а по умолчанию все считается дергающимся снаружи. И если пометить функцию как static, разумеется, эффект пропадает во всех известных мне компиляторах.

Так что, на мой взгляд, все тут в порядке.
UFO just landed and posted this here
Вызов метода по заведомо нулевому указателю — не ошибка??
Нет, я, конечно, последователь метода failfast, но даже он не предполагает, что мы будем ронять приложение заведомо кривыми вызовами…
Вызов метода по заведомо нулевому указателю — не ошибка??

А вдруг нет, если именно такой была цель — вызвать исключение доступа?

А вдруг нет, если именно такой была цель — вызвать исключение доступа?
То у вас случился epic fail, если вы исполняете программу на ARM без MMU. Там по адресу 0 живёт процедура инициализации, которая перезапускает всю систему. Не совсем rm -rf — но почти. И без всякой «помощи» со стороны компилятора…

Ну вот видите, вы даже более полезное применение нашли такому поведению. В чем же тут тогда epic fail?

В чем же тут тогда epic fail?
В том, что ожидаемого поведения (вызвать исключение доступа) вам получить не удалось.

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

Принцип такой: если ваша программа не будет работать на процессорах Cray (где, внезапно, nullptr не состоит из одном нулевых битов), если она не будет ботать на системе с дополнением до одного, если она не сможет работать на процессоре без MMU (где вызов кода по адресу nullptr «уничтожает» вашу программу) и т.д. и т.п. — то эта программа ошибочна.

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

А допустим, вот прямо так. Только Windows и только 32 бита, желаемый уровень абстракции — ну, в общем, такой, на котором можно вызывать WinAPI без обёрток. Какие языки и компиляторы Вы бы посоветовали?

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

Я в целом догадываюсь, что именно такова суровая исторически сложившаяся реальность за пределами тёплой и сухой области применимости условных питона или джавы — или Cray без MMU, или конкретные версии компиляторов, и никаких компромиссов. Но, может быть, что-то пропускаю? Процитированная рекомендация подразумевает что-то третье ("никогда" — это, кажется, не про конкретную версию).

Пока мы говорим про программу в вакууме — да, вы правы. Но как только в дело вступает компилятор, у нас появляется знание о целевой платформе и её свойствах. Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.
Поэтому на этапе компиляции часть UB превращается во вполне себе defined behavior.
Только в том случае если вы это запросили явно. Хотите чтобы переполнение у целых числе не считалось UB? Задайте -fwrapv. Хотите использовать каламбур типизации? -fno-strict-aliasing — и нет проблем.

Но по умолчанию — нет. Программа должна работать на CRAY MP с дополнением до одного — и точка.
Так правильнее будет, не находите?

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


не заменит неправильное "/2" на "*2"

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

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

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

То, что компилятор (стандарт языка) не может магическим образом исправить неправильный код, по-вашему каким-то образом дает ему карт-бланш ломать правильный код?
Любой код, вызывающий UB — неправильный. По определению. Как я уже писал программа, вызвающая UB может исполнять что угодно и когда угодно — причём в стандарте отдельно подчёркнуто, что «когда угодно» — распространяется, в том числе, на время до точки, когда UB произойдёт.
Любой код, вызывающий UB — неправильный.

Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов. Получается «Почти любой код, написанный на Си — неправильный». Так зачем пользоваться таим языком?

Еще раз — почти любой код на Си содержит UB, как минимум из-за прядка вычисления аргументов.
Порядок вычисления аргументов — это UNSPECIFIED behavior, ни в коем разе не undefined.

Так зачем пользоваться таим языком?
Человеку, который путается в терминах и не понимает, что unspecified, undefined и implementation defined — это всё разные вещи с разными последствиями действительно стоит использовать какой-нибудь другой язык, попроще.
Я, конечно, не профи, но все же…
По-моему тут нарушается принцип наименьшего удивления.
Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции. Проще отловить, проще понять, проще исправить.
Эта программа — ошибочна. Проблемы возникают только когда вы вызываете UB. Точка. Конец дискуссии. Как я писал выше — эту программу таки можно использовать так, чтобы не вызывать UB. И вот в этом случае — она будет работать корректно.

А вот как она будет работать если вы вызываете UB — разработчиков компилятора не волнует. Как-то работает — и ладно.
Указатель на функцию можно не только вызвать, но и сравнить с другим указателем или с null. Так что такое преобразование некорректно, если компилятор не проверил отсутствие таких сравнений.
Указатель — локальный для модуля, так что компилятор именно что проделал подобные исследования и потому смог удалить код из NeverCalled.

Добавьте функцию, которая позволить вам на эту переменную посмотреть из другого модуля — и функция NeverCalled станет непустой и переменна Do вернётся. Но из main по прежнему будет вызываться rm -rf, так как ничего другого без попадания на UB вы сделать не сможете.
Как это по-хабовски — минусовать за собственное мнение. Смешно смотреть, как 90% боятся высказаться против 'тренда' из-за боязни слить карму. Но мне пофиг, я не кармадрочер. Аккаунт сгниёт — еще один заведу :) Минусуйте на здоровье :)

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

Падение программы в GPF — меньший (и менее неприятный) сюрприз по сравнению с вызовом не той функции

Это, конечно, вообще ад. В делфи-паскале такое представить сложно. Разве что программер сам накосячит и указатель не на ту функцию передаст. Но это уже его косяк, а не 'особенности' реализации.
UFO just landed and posted this here
UFO just landed and posted this here
Лучше всего, когда слова подкрепляются примерами.
UFO just landed and posted this here
Знаете, сколько я еще интересных названий языков знаю? :) Сравнения какие-то будут? Покажете более выразительную систему типов или более строгую типизацию?
UFO just landed and posted this here
Понятно. Вопросов больше не имею :)
UFO just landed and posted this here
Вашим воззрениям на язык, язык C просто не удовлетворяет. Чем уверять, что язык плохой, просто пользуйтесь другими. У языка C и языка C++ пока ещё есть уникальные свойства, благодаря которым во многих областях их заменить ничем особо не получается.
Со временем, конечно, языки постепенно стремятся к вашему представлению, но это отнюдь не просто.

У вас есть какие-то пруфы относительно того что 90% боятся высказывать свое мнение?

typedef int (*Function)();

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

Вроде ж просто сишный псевдоним для указателя на функцию. Я наоборот видел мало кода, где с указтелями на функции работают без typedef (спасибо "уникальному" синтаксису объявлений си).


Или дело просто в том что есть указатель на функцию?

А не логичнее ли компилятору (или вообще препроцессору) вообще выкинуть неиспользуемую функцию?
А как он узнает, что она не используется? Её могут из другого модуля вызывать.
Так все static от извёл. Он NeverCalled извести не может — но может сделать пустой. Что и было проделано.
Проанализировать исходник аж всего приложения, и увидеть, что в Do значение присваивается лишь однажды — у компилятора мозгов хватило. А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет? Что за странный оптимизатор?
А элементарно то, что функция, в которой есть это самое присвоение никогда не вызывается — нет?

Она может вызваться из другой единицы трансляции. Компилятор — не линковщик, всю программу целиком не видит.
Да, действительно, спасибо. Ведь Do объявлена как static, и поэтому достаточно проанализировать только этот файл. А я ошибочно подумал, что компилятор проанализировал всю программу.
Интересно, кстати, как иногда баяны всплывают. Я только сейчас осознал, что шумиху вокруг этого примера породил я, упомянув его в внутренней рассылке — а кто-то вытащил его на redddit и пошло-поехало.

Так-то оригинал тут — и опубликовали его… давно, в общем.
Добавить к "static Function Do;" volatile и 99% даю, что будет вызываться NULL.
Забавно, что это перекликается с математическим внутренне противоречивым высказыванием. Если из него делать выводы, предполагая, что оно не противоречиво, то можно прийти к чему угодно — из того, что дважды два пять, можно сделать вывод, что у собак есть крылья. Именно так и делает компилятор: предполагает, что в коде UB отсутствует, и получает произвольные удивительные результаты. Понятно, что такое предположение может иногда слегка ускорить правильные программы, но точно ли такая жертва неправильными оправдана? Почему бы не допускать, что программист мог ошибиться, и в программе есть UB?

Насколько я понимаю, изначально UB появилось из-за невозможности гарантировать результат там, где он зависит от архитектуры и других внешних факторов. Но программист может знать, что его программа будет запускаться только на x86, может даже добавить assert на эту тему. На каких-то платформах деление на 0 не вызовет exception, но если компилятор, исходя из этого, будет делать что угодно с программой, пытающейся разделить на ноль на x86, по-моему он будет неправ. Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ, но ещё больше будет неправ компилятор, если выкинет эту проверку как невозможную, или если скомпилирует код в «rm -rf /», хотя формально компилятор при этом не нарушит стандарт.

На C часто пишутся программы без требования переносимости, на то он и низкоуровневый, поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined. Например, при integer overflow может получиться любое число или exception, но не что-либо ещё (например, не «rm -rf /») — такое правило мне казалось бы естественным. Передача управления по NULL — sigsegv на тех архитектурах, где это вызывает прерывание, и UB там, где может выполняться мусор.
Почему бы не допускать, что программист мог ошибиться, и в программе есть UB?
Потому что о правильных программах мы можем судить — в спеке есть описания того, что они должны делать и как. О неправильных же программах мы судить не можем — мы не знаем чего хотел программист и очень часто программист этого тоже не знает. Попытки же «выудить» из него эту информацию тоже добром не кончаются ибо разные программисты ожидают разного поведения, когда их программа совершает UB.

Но программист может знать, что его программа будет запускаться только на x86, может даже добавить assert на эту тему.
А может также ничего не знать. А ещё может оказаться, что нам нужно будет эмулировать тонкие краевые эффекты. Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0. Если у нас кросс-компилятор и мы полагаемся на то, что UB в программе нет, то мы можем сдвигать когда угодно и что угодно, так как программист должен будет обеспечить, чтобы сдвига на 33 в программе не случилось. Если же мы разрешим такие сдвиги, то придётся либо писать эмулятор «сдвигов типа ARM» на x86 и использовать его при рассчёте констант, либо отказаться от их рассчёта.

Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ
Если к unsigned, то он прав. И формально и фактически.

На C часто пишутся программы без требования переносимости, на то он и низкоуровневый, поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.
Позиция разработчиков компиляторов проста и незатейлива: хотим чего-то подобного — пишем пропозал — получаем результат.

Просто потому что ссылки на здравый смысл не работают. Он у всех разный, как показывает этот тред…
> Если к unsigned, то он прав. И формально и фактически.

О, вот хороший пример. Я знаю, как оно на низком уровне, и знаю, что integer overflow — это UB. Поэтому не удивлюсь, если и для unsigned int переполнение окажется UB. По стандарту не оказалось, фух. Но если для signed переполнение UB — это всё равно минное поле.
> Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ

Именно для unsigned правила выглядят так:

C99 пункт 6.2.5.9:

>> A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.

Аналогичное правило в C++11, пункт 3.9.1.4:

>> Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2n where n is the number of bits in the value representation of that particular size of integer.

А вот для signed они одинаково (косвенно) определяют, что переполнение — UB.

Как результат, можно проверять переполнение (сильно менее эффективно, чем напрямую машинными средствами) через перевод в unsigned и изучение результата; и точно так же можно реализовывать «заворачивающуюся» арифметику.

С другой стороны, я согласен с общей идеей. В результате подобного подхода со стороны стандартизаторов и авторов компиляторов, поворачивающих «закон» в свою сторону, я уже слышал много сообщений типа «ну вас нафиг, ухожу на Java/C#/Go/etc.» именно за счёт гарантий, которые даёт эта группа; часто их даже не интересует managed memory — их задалбывает мир, где любой неосторожный шаг приводит к падению в пропасть.

> поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.

+100.
Давайте честно скажем, что в шланге багло, несмотря на Undefined behavior, компилятор берёт на себе излишне много.
Давайте скажем честно: это камлание из серии «собака лает — караван идёт». В точности нуль разработчиков придерживаются этой точки зрения. То есть ни одного «сочувствующего» вы не найдёте.

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

Любовь программистов, выросших на Java, устраивать 100500 индирекций делает подобные оптимизации весьма полезными, так что ломать их ради программы с ошибкой — никто не будет.
> В точности нуль разработчиков придерживаются этой точки зрения. То есть ни одного «сочувствующего» вы не найдёте.

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

или целевых программ — их пользователей?
Как любит говорит Линус: Talk is cheap. Show me the code.

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

Я думаю что netch80 не спорил с неадекватностью этой точки зрения, а как раз говорил что среди прикладных программистов большинство толком не понимают как вообще работает компилятор, так что "ни одного «сочувствующего» вы не найдёте." — не верно, потому что их полно (и это печально).

И да, и нет. Безусловно, большинство прикладных программистов не понимают, как работает компилятор; но им обычно это и не нужно, нужно иметь положительную часть опыта (как делать) и отрицательную (как не делать, где грабли). Но основное таки не в работе компилятора, а в том, когда он становится усилителем ошибок. Пример в исходном постинге темы не настолько характерен, как, например, этот; см. по тексту, как или изменение опций компиляции, или небольшая правка исходника, не меняющая суть выполняемого, сменяет неограниченный цикл на ограничение 3 итерациями в цикле. Вот это случай, когда возможность сделать UdB откровенно абьюзится авторами компилятора, а программисту найти такое, если кода много и/или оно закопано в макросах, может быть очень тяжело.

Ну а поскольку безошибочных программ вообще не бывает (helloworldʼы не считаем, и то неизвестно, что там в libc) — совершенно очевидно и обоснованно создаётся ощущение минного поля, авторы которого тут же с краю стоят и усмехаются — «ну-тко, кто ещё на что нарвётся?» А с соседнего поля кричат «а у нас мин нет, а ещё есть печеньки (вариант: батарейки)»…
Я бы проводил аналогию не с минным полем, а со слесарной мастерской, уставленной мощными станками: если неаккуратно пользоваться, оставит без рук. А из соседней мастерской кричат «а у нас все инструменты надувные, даже не ушибёшься!»
В «соседней мастерской» не надувные, а реальные и делающие ровно то, что им задают. А в мастерской C — такие, что если на них хоть чуть-чуть ошибся, они через полчаса (когда ты давно завершил кусок работы и занялся другим) выстрелят, совершенно законно, тебе сверлом в спину.
> Совершенно непонятно откуда возьмётся компилятор, что-либо делающий по-другому, если его некому разрабатывать. Ну вот совсем некому.

Так есть же кому. Только они в результате уходят и создают своё. Например, читаем спеку на Go:

>> For signed integers, the operations +, -, *, and << may legally overflow and the resulting value exists and is deterministically defined by the signed integer representation, the operation, and its operands. No exception is raised as a result of overflow. A compiler may not optimize code under the assumption that overflow does not occur. For instance, it may not assume that x < x + 1 is always true.

По последнему предложению видно, что это прямой наезд на подходы C/C++. И далее:

>> Shifts behave as if the left operand is shifted n times by 1 for a shift count of n.

и никаких тебе «если >= ширине сдвигаемого, мы включаем джаз». (В отличие от Java, C#, где заворот для знаковых целых определён, а вот правила для сдвигов уже как в C.)

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

А ещё в Java, C#, Go, etc. жёстко определено, что размерность целых — степень двойки, а представление отрицательных — дополнительный код. И это не мешает им работать на >99.99% реально существующих платформ, включая супер-embedded типа SIM-карт.

Зачем предполагать то, что в реальности уже не существует? Вы можете назвать хоть одно реальное железо, где остались бы отрицательные целые в обратном коде (1ʼs complement)? И почему, с обратной стороны, C завязан на двоичные биты? Почему (извините за провокацию) не рассчитывают на машины, у которых только десятичные цифры, или на троичные, типа «Сетунь»? По-моему, распространённость машин с обратным кодом примерно равна распространённости «Сетуни», то есть нулю.

А даже если там не 0 — то насколько это важно по сравнению с основной массой? Не лучше ли создать профиль, покрывающий практически всех?

> никто из людей, громко тут «бурлящих» не имеет ни малейшего представления о том, что у него «под капотом» и как оно там работает!

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

Только не надо, пожалуйста, говорить «кто ниасилил — кыш на другие языки». Это уже и так происходит, к сожалению. Хотелось бы, наоборот, чтобы эту миграцию никто не форсировал.
Так есть же кому. Только они в результате уходят и создают своё. Например, читаем спеку на Go:
Какое имеет отношение «спека на Go» к C/C++?

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

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

Просто потому что разные процессоры ведут себя по разному, но ни один (из распространённых) не ведёт себя так, как предписывает спека на Go!

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

И это не мешает им работать на >99.99% реально существующих платформ, включая супер-embedded типа SIM-карт.
Серьёзно? Да одних роутеров и «умных» лампочек, на которых никакие C#/Java и «не ночевали» больше, чем всех «платформ», на которых они работают! Код на C и даже C++ работает на гораздо, гораздо, ГОРАЗДО большем числе платформ, чем код на C# или Java.

Зачем предполагать то, что в реальности уже не существует?
Это вопрос скорее к комитету по стандартизации. На практике же особо ничего для поддержки подобных платформ не делается. Остался только артефект: беззнаковое переполноение не существует, а знаковое — это UB. Но к этому, в общем, все уже привыкли.

Только не надо, пожалуйста, говорить «кто ниасилил — кыш на другие языки». Это уже и так происходит, к сожалению. Хотелось бы, наоборот, чтобы эту миграцию никто не форсировал.
А её никто и не форсирует. Просто пока есть медицинский факт: компиляторы, которые «строже» наказывают за UB, чем другие — в числе наиболее популярных. Потому что они, ко всему прочему, быстрее и функциональнее. Попытавшись «удержать» пользователей, которые хотят «безопасности и предстказуемости» — есть шанс, что их всё равно не удержат (ибо C/C++ — в любом случае относится к категории сложных и небезопасных языков), но при этом потеряют тех, кому важна скорость.
чтобы поддержать это определение вам нужно в коде, в каждом месте, где вызывается сдвиг иметь маленький кусочек кода, который именно что и будет делать проверки на тему "если >= ширине сдвигаемого"

Зачем, если проверки и соответствующее поведение и так находятся в процессоре?

Как раз сейчас не находятся (в основных командах сдвигов): они игнорируют старшие биты, а для результата «результат сдвига на N бит равен результату N сдвигов на 1 бит» должны не игнорировать.

Кодогенератор Go (он у них свой, доморощенный) реализует это следующим образом: пусть у нас вход:

        var a uint32
        var s uint32
        fmt.Printf("Params? ")
        fmt.Scanf("%x%d", &a, &s)
        r1 := a << s
        r2 := a >> s
        r3 := uint32(int32(a) >> s)
        fmt.Printf("%d(%x) %d(%x) %d(%x)\n",
                r1, r1, r2, r2, r3, r3)


Собственно вычислительная часть выходного кода выглядит так:

дизассемблер с пояснениями
e8 d5 a3 ff ff          callq  48d8b0 <fmt.Scanf>
48 8b 44 24 60          mov    0x60(%rsp),%rax ; &a
8b 00                   mov    (%rax),%eax ; a
48 8b 4c 24 58          mov    0x58(%rsp),%rcx ; &s
8b 09                   mov    (%rcx),%ecx ; s
89 c2                   mov    %eax,%edx ; a
d3 e0                   shl    %cl,%eax ; a << s машинный
83 f9 20                cmp    $0x20,%ecx
19 db                   sbb    %ebx,%ebx ; (s<32)?-1:0
21 d8                   and    %ebx,%eax ; r1 = a << s
89 44 24 54             mov    %eax,0x54(%rsp) ; for Printf
89 44 24 50             mov    %eax,0x50(%rsp) ; for Printf
89 d0                   mov    %edx,%eax ; a
d3 ea                   shr    %cl,%edx ; a >> s машинный
21 da                   and    %ebx,%edx ; r2 = a >> s
89 54 24 4c             mov    %edx,0x4c(%rsp) ; for Printf
89 54 24 48             mov    %edx,0x48(%rsp) ; for Printf
f7 d3                   not    %ebx ; (s>=32)?-1:0
09 d9                   or     %ebx,%ecx ; (s>=32)?31:s
d3 f8                   sar    %cl,%eax ; signed_a >> s
89 44 24 44             mov    %eax,0x44(%rsp) ; for Printf
89 44 24 40             mov    %eax,0x40(%rsp) ; for Printf


Код, как для современных процессоров, построен в стиле branch-free. Вычисляется «флаговое значение», которое равно -1 (все единичные биты), если сдвиг в пределах ширины переменной (32), и 0, если сдвиг равен этой ширине или выше; логический AND зануляет результат в случае слишком широкого сдвига. Для сдвига знакового значения вправо ещё хакеристее: так как результаты сдвигов на s>=31 совпадут со сдвигом на 31, это флаговое значение используется, чтобы сдвиг заменить на 31, если он больше, и дальше используется уже машинная команда.


И это то, что я хотел бы видеть по умолчанию для операции сдвига в любом языке уровня от C++ и выше. Ибо POLA и естественность для человека. А если кому-то нужно гарантировать оптимизированность операции — например, транслятор не может понять, что сдвиг будет в нужных пределах — предоставить какие-нибудь int::native_sll(arg, shiftcount), который будет builtinʼом транслятора.
они игнорируют старшие биты
Если бы они все хотя бы игнорировали биты — это было бы полбеды. А так — x86 игнорирует все «лишние» биты и сдвиг 32-битного числа на 33 — это то же самое, что сдвиг на 1. А ARM — игнорирует, но не все: сдвиг на 33 — отрабатывается правильно, а вот сдвиг на 257 — это опять то же самое, что и сдвиг на 1.

Собственно поэтому это и UB в C/C++.

А если кому-то нужно гарантировать оптимизированность операции — например, транслятор не может понять, что сдвиг будет в нужных пределах — предоставить какие-нибудь int::native_sll(arg, shiftcount), который будет builtinʼом транслятора.
Если вы делаете свой, новый, язык «с иголочки» — то вы можете себе это позволить. Но если язык у нас уже есть и на нём написаны миллиарды строк кода — то глупо как-то его брать и замедлять на ровном месте…
> Какое имеет отношение «спека на Go» к C/C++?

Такое, что Go позиционируется, по одному из каналов, как «безопасный C с шахматами и поэтессами». И это реально работает, по тому, что я вижу, в плане миграционных тенденций.

> И ситуации, когда у вас будет быстрый, но небезопасный компилятор и медленный, но безопасный — у вас не будет. Ибо все компиляторы будут медленными…

— Моя программа работает в 4 раза быстрее твоей!
— Зато моя программа работает правильно.

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

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

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

> Просто потому что разные процессоры ведут себя по разному, но ни один (из распространённых) не ведёт себя так, как предписывает спека на Go!

Ха, ошибаетесь :) Делает, и самый распространённый. Почитайте для x86 доку на семейство PSL{L,R}{W,D}. Если сдвиг шире, чем одиночное значение в векторе, выходное значение обнуляется.

Я не знаю, зачем они это сделали, какой юзкейс стоял над ними. Могу только догадываться, что, как для всего MMX/SSE, они оптимизировали какой-то сверхважный частный алгоритм. Там много непонятного, включая мнемонику — они не стали повторять обычную скалярную x86, а взяли привычную для RISC. И, конечно, чисто формально это никак в данном споре не влияет на общий результат. Но говорить, что «ни один», нельзя.

А ещё Вы говорили рядом:

>> Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0.

Проясните? Это тот же случай, или это ARM64 со сдвигами только двойными словами?

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

Да. Только этого ли хотели добиться, ужесточая наказания за ошибку?

> Да одних роутеров и «умных» лампочек, на которых никакие C#/Java и «не ночевали» больше, чем всех «платформ», на которых они работают!

Потому что никто не видел окупаемой цели в этом переносе. Но не по чисто технической невозможности.

> Остался только артефект: беззнаковое переполноение не существует, а знаковое — это UB. Но к этому, в общем, все уже привыкли.

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

Вот все эти ubsanʼы — начало действительно конструктивного подхода. Начало — потому, что само их наличие до сих пор малоизвестно, а регулярное использование никак не всеобщая практика. В идеале основные жалобы на проблемы должны быть сгенерированы ещё на стадии трансляции.

А если говорить про уровень самого языка — в идеале то, что я хотел бы видеть, это некоторое расширение подхода, как в C# checked/unchecked, только с бо́льшим количеством вариантов. Например, для целых — выбирать раздельно по signed/unsigned исполнение арифметики: wrapping, checked, relaxed (как сейчас signed в C — гарантируется непереполнение), platform native, saturating (последнее — опционально). Тогда, записав
a = [[signed_checked]] (b+c); 

мы получаем конкретную желаемую программистом локальную политику, пусть даже это замедлит выполнение.
И, разумеется, всё это на уровне стандартного языка, а не расширениями вроде -fwrapv, и безотносительно общего уровня оптимизации.
Извините за почти бесплодные мечтания (по поводу C).

И спасибо за признание, что UdB для знакового переполнения — это именно рудимент от времён динозавров.

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

Про скорость всупереч корректности я уже сказал выше. Пока что всё это не отменяет того вывода, что сишники своими руками прогоняют тех, кто при относительно небольших усилиях авторов трансляторов мог бы остаться их пользователем. Да, я тут самонадеянно считаю, что описанные выше подходы управляемого уровня агрессивности транслятора реальны и подъёмны. Тот же -fwrapv существует уже много лет, чем эти опции хуже?
Такое, что Go позиционируется, по одному из каналов, как «безопасный C с шахматами и поэтессами».
«Позиционироваться» и «являться» — разные вещи.

И это реально работает, по тому, что я вижу, в плане миграционных тенденций.
Вот только есть одно «но»: вы-то это видите, а Роб Пайк — не видит. Вот тут он как раз удивляется этому феномену: он-то делал Go, как «правильный C» — а получил, почему-то, в основном, перебежчиков с Python'а и Ruby. Да что там говорить: в нашем проекте часть вещей была переведена с pyhton'а, но го, но что было на C/C++ — осталось на C/C++.

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

Почитайте для x86 доку на семейство PSL{L,R}{W,D}. Если сдвиг шире, чем одиночное значение в векторе, выходное значение обнуляется.
Особенно это хорошо работает для байтов, да. И в любом случае кто-нибудь обломается, обнаружив, что сдвиг работает не как VSHL на ARM'е (If the shift value is positive, the operation is a left shift. If the shift value is negative, it is a truncating right shift.)

Стоит ли говорить о том, что в реальных программах сдвиг, почти всегда, хранится в переменной со знаком (обычно int)?

Но формально — да, вы правы, признаю, был неточен…
А ещё Вы говорили рядом:

>> Например 32-битная единица, сдвинутая вправо на 33 бита на x86 даст 2, а на ARM'е — 0.

Проясните? Это тот же случай, или это ARM64 со сдвигами только двойными словами?
Тот же, но не совсем. То есть для чисел до 255 всё работает как в Go. Однако. В ARM2 в barrel-shifter подавался только младший байт операнда. Во всех процессорах (включая самые последние) сдвиг работает так же. Совместимость-с.

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

Да. Только этого ли хотели добиться, ужесточая наказания за ошибку?
Нет, конечно. Никто никого «ужесточать» не пытается. Пытаются сделать более быстрый компилятор. Достаточно успешно: clang сейчас уже чаще используется, чем GCC.

Потому что никто не видел окупаемой цели в этом переносе. Но не по чисто технической невозможности.
Технически можно и Windows 10 на Commodore 64 запустить. Вот только не делает никто почему-то.

А если говорить про уровень самого языка — в идеале то, что я хотел бы видеть, это некоторое расширение подхода, как в C# checked/unchecked, только с бо́льшим количеством вариантов. Например, для целых — выбирать раздельно по signed/unsigned исполнение арифметики: wrapping, checked, relaxed (как сейчас signed в C — гарантируется непереполнение), platform native, saturating (последнее — опционально). Тогда, записав
a = [[signed_checked]] (b+c);

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

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

Разработчики компиляторов — они не садисты, у них нет цели кого-либо наказать. Просто когда в 99.999% случаев всё и так работает без всяких «лишних» проверок (как в примере со сдвигами выше), то глупо на них тратить время — особенно если стандарт этого не требует.

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

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

Пока что всё это не отменяет того вывода, что сишники своими руками прогоняют тех, кто при относительно небольших усилиях авторов трансляторов мог бы остаться их пользователем.
Я вот в этом совсем не уверен. Ибо они гонял-гонят, а прогнать никак не могут. На TIOBE C и C++ — по прежнему языки #2/#3, а Go и D — где-то во втором-третьем десятке. Правда относительная популярность падает — но она и у Java падает так же стремительно, так что сдаётся мне, что не в UB дело.

Тот же -fwrapv существует уже много лет, чем эти опции хуже?
Если они всего лишь «не хуже», то смысла в них особого нет. Сейчас проверил: в исходниках Android'а -fwrapv используется в 5 проектах: syslinux, dEQP, Python и mksh. Ни один из них не является критичным и при необходимости от них от всех можно отказаться.

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

Переформулирую то же самое с другого угла зрения: компиляторы разрабатываются вполне прагматичными людьми для вполне практических целей. Тот, кто добавлял в Clang оптимизацию, ставшую темой поста — или любую другую оптимизацию — уж точно не потирал ладони: «уж теперь-то вы у меня попляшете, жалкие людишки, допускающие в своём коде UB!» Никто не станет реализовывать абсурдные трансформации кода лишь затем, чтобы досадить программистам — независимо от того, допускает стандарт такие трансформации или нет.
Наоборот: если какая-то оптимизация была реализована и продолжает поддерживаться, это значит, что в реально компилируемом коде она полезна, даже если в отдельных «лабораторных» примерах она кажется абсурдной.
Ну-ну, незачем быть настолько агрессивным, отстаивая правильность поведения при «неопределённом поведении». Фактически компилятор ведёт себя максимально неочевидным способом. В точности нуль разработчиков ожидает то, чего они получат и никого это не устроит. Детали реализации пользователей компилятора не волнуют, как и ваши проблемы с некими программистами, выросшими на Java, храни их боже.
Ну-ну, незачем быть настолько агрессивным, отстаивая правильность поведения при «неопределённом поведении».
Для того, чтобы не получать агрессию не нужно начинать с бессмысленых наездов. Когда вся статья, фактически, является развёрнутым описанием того факта, что ошибок в компиляторе нет и ошибка, собственно, допущена программистом комментарий вида давайте честно скажем, что в шланге багло может восприниматься либо как неумение читать, либо рафинированное д’Артаньянство. Даже не знаю что хуже.

Ещё раз: неопределённое поведение на то и «неопределённое», что любое, какое угодно поведение — будет допустимо. В этом его суть и смысл.

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

Детали реализации пользователей компилятора не волнуют, как и ваши проблемы с некими программистами, выросшими на Java, храни их боже.
А правильность работы программы их волнует? Тогда им нужно запомнить: программа не должна вызывать неопределённого поведения. Никогда. Ни при каких условиях. Пресловутый nethack запускался GCC, прости господи, в 1988м году. чтобы это подчеркнуть! Это не что-то, что разработчики вчера придумали — и начали «всё портить»!

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

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

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

> но почему их мнение должно кого-то волновать?

Хотя бы потому, что они — практические пользователи полученного. И позиция "сперва добейся" их не устраивает.
И Вы тоже тут включили режим дʼАртаньяна, скопом записав всех возражающих в невежды.
В моём случае это сухая констатация факта. Если вы знаете хоть одного человека, который бы засабмитил хотя бы малюсенькое изменение в clang или gcc и который, по отношению к данному примеру, разделеяет мнение «давайте честно скажем, что в шланге багло» — то я готов изменить своё мнение и с ним уже, конкретно и предметно, обсуждать альтернативы.

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

Как уже говорилось тут много раз: хотите иного поведения — напишите соотвествующий пропозал. Здравый смысл, он, знаете ли, у всех разный, потому в качестве ориентира ну никак не годится…
Ещё раз повторяю, любое неочевидное поведение продукта, который вводит пользователя продукта в глубокий ступор — бесспорное багло. В данном случае очевидное донельзя. Отрицая это, Вы ставите себя в весьма глупое положение.
Ещё раз повторяю, любое неочевидное поведение продукта, который вводит пользователя продукта в глубокий ступор — бесспорное багло.
Вы можете повторять сколько угодно, но суть от этого не изменится: профессиональные продукты требуют к себе профессионального же отношения. Будь то какая-нибудь зеркалка со сменными объективами или компилятор C++. Хотите пользоваться инструментами, которые не «вводят пользователя в ступор» — берите мыльницу или там LD. Может ещё Java подойдёт, я не знаю. Но не C/C++. К ним спека полагается и её полагается соблюдать.

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

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

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

Это будет значительно более корректной аналогией для undefined behavior.

> профессиональные продукты требуют к себе профессионального же отношения

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

Продукт, который взрывается, например, от арифметического переполнения от того, что изменился множитель в далёком заголовочном файле (и сделал это совсем другой коммиттер) — не должен считаться профессиональным.
UFO just landed and posted this here
> Решение-то очень простое: не покупайте и не садитесь за руль таких самосвалов (или языков).

О чём я и говорю тут давно.

> А если вам потребуется вся мощь этого самосвала (производительность этого языка),

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

> и радуйтесь жизни.

не получится — пока в системе есть хоть что-то на C/C++, доверие к ней уже страдает.
не получится — пока в системе есть хоть что-то на C/C++, доверие к ней уже страдает.
Ну так что мешает переписать всю систему так, чтобы в ней не было C/C++?

Большая работа, конечно, но за то время, пока вы тут высказываете своё «фэ» вы могли бы уже что-нибудь да написать…
UFO just landed and posted this here
Профессиональные продукты точно так же рассчитаны на то, что их использует человек — и даже больше рассчитаны, у них эргономичность выше, и компенсируют типичные проблемы человека.
На то, что их использует человек — да, рассчитаны. На то, что их использует неуч, не знающий как раз полученный снимок влияете увеличение/уменьшение выдержки и «игра с диафрагмой» — нет. Хотите пользоваться профессиональным аппаратом — извольте изучить теорию.

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

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

Это будет значительно более корректной аналогией для undefined behavior.
О! Прекрасная аналогия, а главное, очень точная. Только не на полсекунды дольше, конечно, а на полсекунды меньше.

Главное-то: стояночные тормоза на карьерных самосвалах реально рассчитаны на уклон в 15%, не больше (иначе очень удорожается конструкция) и если кто-то, сдуру, оставит их на склоне в 30% — то самосвал таки «сползёт» и может и наделать кучу разрушений. А виноват будет водитель и никакие жалобы на то, что «я свою малолитражку 100 раз на подобном склоне бросал» в рассчёт браться не будут. UB — они такие, что у самосвалов, что у языков программирования…
Да нет, уважаемый коллега, я весьма неплохо орудую C++, крайне неплохо, рискую показаться грубым, но я бы поумерил ваш пыл в мою сторону, однако, как и любой пользователь, я люблю такого же уважительного отношения ко мне как к пользователю, какое проявляю сам к пользователям моих систем. Если пользователю моего продукта что-то неочевидно, виноват я, так и тут, очевидно, что шланг ведёт себя крайне неестественно и оптимизацией подменяет наиболее очевидное поведение. Можно сколько угодно прикрываться стандартом, тут это шлангу никак не помогает.
как и любой пользователь, я люблю такого же уважительного отношения ко мне как к пользователю, какое проявляю сам к пользователям моих систем
Извините, но тут вы опять своё частное мнение «растягиваете» на всех. Я вот, как и многие другие читатели этой статьи активно не хотим, чтобы ради вашего ублажения clang отказывался от полезных оптимизаций.

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

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

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

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

Можно сколько угодно прикрываться стандартом, тут это шлангу никак не помогает.
Что значит «не поможет»? Ему не нужно помогать, он просто работает. Так, как в данном конкретном случае, и должен. В нём есть ошибки, большая и сложная программа всё-таки, но данная конкретная оптимизация — ошибкой не является. Вернее, конечно, является: ошибкой программиста, написавшего этот безумный код. Он должен эту ошибку исправить и больше так не делать, вот и всё…
Ну не сказал бы я, что он прям уж работает. Претензий к нему стабильно больше, чем к GCC и MSVC, пока он на уровне «так себе поделка». Данный пример, с вашим комментарием
Вопрос в другом: а хочу ли я, чтобы он так делал? И в моём случае ответ однозначен: да, разумеется.

однозначно определяет дальнейшее развитие продукта в сторону наиболее косячную с итераионным развитием в сторону «ну да, неочевидно, странно, но зато оптимизировано в другом месте». Подход весьма скоро приведёт Вас к взрослению, когда пезанская башня начнёт очень сильно наклоняться.
Для сравнения подходов, крайне рекомендую покодить полгодика-годик на том же Python 3.x, весьма освежает подход к очевидному и понятному и заставляет по-другому взглянуть на свой код на C++.
Также было бы полезно попрактиковаться в Boost.Python, там всё сделано для удобства пользователя, каким бы программистом он ни был. Сравните подход, его аккуратность и очевидность происходящего в процессе написания кода.
Претензий к нему стабильно больше, чем к GCC и MSVC, пока он на уровне «так себе поделка».
Претензий к нему больше ровно потому, что он работает. Начиная от SDK (где для большинства популярных платформ используется именно clang сегодня) и кончая всякими Google'ами и Facebook'ами (Google, насколько я знаю, вот прямо сейчас находится в состоянии перехода на clang — финальная стадия: отказ от «страховочной сборки» с помощью GCC на билдботах).

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

Для сравнения подходов, крайне рекомендую покодить полгодика-годик на том же Python 3.x, весьма освежает подход к очевидному и понятному и заставляет по-другому взглянуть на свой код на C++.
«Полгодика-годик» для языка программирования мало — этого как раз достаточно, чтобы увидеть в языке «красивые плюшки», но недостаточно для того, чтобы понять, какие в нём таятся проблемы.

Мы в своё время достаточно активно использовали Python, но, насколько мне известно, сейчас создавать новые проекты на нём просто запрещено. Ибо python — это такой себе язык для «волка-одиночки»: на нём прекрасно можно писать даже многого о нём не зная — но зная в мельчайших деталях свою собственную программу. После ухода с проекта людей, изначально написаваших код, проект на Python, как правило, «рассыпается». И довольно быстро. Рекорд, кажется, 3 года с момента ухода с проекта его инициатора.

Также было бы полезно попрактиковаться в Boost.Python, там всё сделано для удобства пользователя, каким бы программистом он ни был. Сравните подход, его аккуратность и очевидность происходящего в процессе написания кода.
Сравнили. Прониклись. Для режима тяп-ляп-и-в-продакшн — годится, но для этого вообще годится почти всё, даже, прости госсподи, PHP.

А для проектов, которые должны поддерживаться не один десяток лет и, главное, не теми людьми, которые их создали изначально — не годится. Вот такой вот порадокс: C++ и Java — годятся (причём между любителями этих языков периодически происходят споры на тему, что лучше в долгосрочной перспективе… но есть как проекты, перешедшие с C++ на Java, так и проекты, перешедшие с Java на C++), а Python (и даже связка C++/Python) — не годится. Причём не годится категорически! Go — в процессе «обкатки» и, похоже, останется — но не как замена для C++, а, скорее, как замена Python'у для [относительно] простых программ.
> После ухода с проекта людей, изначально написаваших код, проект на Python, как правило, «рассыпается». И довольно быстро. Рекорд, кажется, 3 года с момента ухода с проекта его инициатора.

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

> а Python (и даже связка C++/Python) — не годится. Причём не годится категорически!

;)
Что там у вас за специфика дружбы с питоном — непонятно, но где-то вы катастрофически перегнули…
Тут не вопрос «дружбы с python'ом», а вопрос поддержки проекта другой командой.

Начальный автор ушёл через полтора года после старта, и меня там уже года три как нет (но я в курсе общей картины).
Это, конечно, внушает, хотя смущает вот как раз замечание в скобках. Сколько раз за это время проект передавался, условно, из офиса в Нью-Йорке в офис в Сиднее? То есть не «наняли человека, он за 2-3 кода „пропитался духом“, через через 2-3 года ушел, команда в целом сохранилась», а «офис в Нью-Йорке оказался слишком дорогим и всё разработку перевели в Сидней» (где, понятно, совсем другие люди).

Это нормальная ситуация в крупных проектах — и именно то, чего проекты на python'е категорически не переживают.

А небольшие проекты, как я уже сказал, можно хоть на brainfuсk'е поднять, если достаточно хорошие специалисты по нему будут…
Ну начинаются стандартные отмазы: для тяп-ляп разработки, для наколенных приложений, синтаксический сахар, язык не язык, платформа не платформа. Дабы не быть голословным, я создал распределённую систему для написания методов бизнес-логики на Python ещё в 2011 году, до сих пор без меня прекрасно работает, поддерживаемая и со стороны ядра на C++, и со стороны Python большинство разработчиков. Зато не так давно загнулась крупная разработка, где ребята, закончившие мехмат наговняжили на C++ огромную нерабочую систему. Ведь на C++ куда проще накосяпорить, если нет понимания. И не будет понимания, пока не с чем сравнивать. Нужна крупная понятно написанная до очевидного система. Возьмите код Django. Даже не обязательно Python, возьмите AR на RoR, тоже отличная система. Потом посмотрите на ваш код, сравните и ужаснитесь. У меня всё.
Ну начинаются стандартные отмазы: для тяп-ляп разработки, для наколенных приложений, синтаксический сахар, язык не язык, платформа не платформа.
Ну да — банальности. Которые, собственно, являются банальностями потому, что они являются правдой.

Дабы не быть голословным, я создал распределённую систему для написания методов бизнес-логики на Python ещё в 2011 году, до сих пор без меня прекрасно работает, поддерживаемая и со стороны ядра на C++, и со стороны Python большинство разработчиков.
И снова: каковы масштабы?

Зато не так давно загнулась крупная разработка, где ребята, закончившие мехмат наговняжили на C++ огромную нерабочую систему.
Вот это — уже интереснее. Она «не взлетела» или таки «стала популярной (пусть в узких кругах), а потом они не смогли её поддерживать»?

Ведь на C++ куда проще накосяпорить, если нет понимания.
Мой опыт говорит о строго оборатном. Да, «накосячить» достаточно легко — но в этом случае, с вероятностью 90%, вы не доведёте дело даже до первого релиза. Это — собственно основной механизм защиты и выживания C++-проектов: плохо написанные проекты умирают до своего взросления, тем самым снижая общие затраты на них (исправление ошибок после релиза на порядок дороже, чем до).

Нужна крупная понятно написанная до очевидного система.
Ну, в общем-то, да, именно так.

Возьмите код Django. Даже не обязательно Python, возьмите AR на RoR, тоже отличная система.
Они относительно неплохо спроектированы, но уже на таких масштабах необходимость владения «tribal knowleadge» и проблемы поддержки начинают ощущаться.

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

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

Я не обвиняю ни Django, ни RoR, но когда мне в качестве «крупного проекта» предъявляют нечто на 300 тысяч строк (притом, что для меня 100 тысяч строк — это явно «мелкий», хотя, возможно, и важный, компонент… большинство субпроектов у нас измеряют количество строк миллионами)… то я не «ужасаюсь», а смеюсь.

В том-то и дело, кто критерий крупного проекта прост: это нечто, чего не может «вместить в одной голове» никакой разработчик. Ни один из приведённых вами примеров туда ну никак не попадает.

А системные языки (начиная с PL/I и далее через C к C++) — для этого как раз и предназначались. И успешно используются.
Ну, если вы пишете бизнес-логику на плюсах, то это уже клиника. Сорян. Лайнер выглядит лайнером, только размерами кодовой базы. Простые операции с коллекциями — ад, простейшие операции с текстом в Юникоде — ад. Не в той вы рыбацкой лодке.
Что до проекта, если вы в России, то скорее всего ваша компания почти наверняка сдаёт всю отчётность нашему государству через код на С++/Python.
С распределением нагрузки (перед 1-м апреля нагрузка будь здоров), с кучей воркеров, асинхронностью, и всем тем, что вы называете синтаксическим сахаром, дабы не дать разработчику моей позапрошлой компании ошибиться хоть где-либо.
Это вам не по вектору итераторы гонять. :)
P. S. Думайте и дальше, что на Python какие-то рыбацкие лодки, а на C++ можно построить что-то исключающее фатальные ошибки. При всех ваших косяках архитектуры можете обвинять криворукого пользователя, который чего-то там недопрочитал. Удач.
Ну, если вы пишете бизнес-логику на плюсах, то это уже клиника.
Бизнес-логика пишется на тех языках, которые позволяют достичь заданных бизнес-пераметров, извините. Если у вас ускорение ответа на миллисекунду приносит несколько миллинов долларов в год — проблемы с UB не кажутся такими уж страшными на фоне возможной прибыли.

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

Лайнер выглядит лайнером, только размерами кодовой базы.
Ну дык с реальными кораблями так! Чем, в сущности, Harmony of the Seas отличается от спортивного каяка? По большому счёту только размерами и оборудованием. Однако ж людям, хорошо проходящим на каяке горные перегоны вряд ли доверят Knock Nevis… хотя и капитан Knock Nevis в похоже на горной речке может не справиться с управлением, конечно…

С распределением нагрузки (перед 1-м апреля нагрузка будь здоров), с кучей воркеров, асинхронностью, и всем тем, что вы называете синтаксическим сахаром, дабы не дать разработчику моей позапрошлой компании ошибиться хоть где-либо.
О! Это уже предметно. Когда один из наших клиентов запускал годик назад игру на один из наших сервисов свалилась дополнительная нагрузка в 1000000 QPS вместо запланированных 100000 QPS (прописью, если у вас от ноликов в глазах рябит: вместо ста тысяч запросов в секунду мы получили миллион). За три дня перенастроили всё и ситуация вошла в норму.

Как ваша «будь здоров нагрузка» с этом соотносится и сможет ли ваш «синтаксический сахар» подобное переварить?

P. S. Думайте и дальше, что на Python какие-то рыбацкие лодки, а на C++ можно построить что-то исключающее фатальные ошибки.
Думайте и дальше, что Python, написанный на C, работающий на операционке, написанной на С/C++ и взаимодействующий с вервисами, написанными на C/C++ каким-то магическим образом окажется безопаснее, чем хорошо протестированная и профаззенная программа на C/C++…
UFO just landed and posted this here
Ну если Вы предпочитаете Хаскель Питону, то мне даже сказать нечего. Удачи в том, что Вам точно мозги прочищает. Тут явно не прочистка нужна.
Что до того, что clang работает быстрее, он хорош пока работает номально. Сюрпризов он выдаёт немало, а в продакшене сюрпризы строго противопоказаны.
Что до того, что clang работает быстрее, он хорош пока работает номально.
Он уже очень и очень давно «работает номально». За исключением версии под Windows, правда. Ну и если вы спеки соблюдаете, когда код пишите…

Сюрпризов он выдаёт немало, а в продакшене сюрпризы строго противопоказаны.
То есть для проектов с миллиардами пользователей (Android, iOS, Chrome, Google Search и т.д. и т.п.) — clang годится, а для вашего «бизнес-процесса»… катастрофа.

Вы уверены, что проблема в clang'е?
UFO just landed and posted this here
UFO just landed and posted this here
> Я пишу код, «положив с прибором» на все стандарты, учебники и прочие статьи (коих в интернете десятки), а компилятор, тем не менее, обязан меня «понять и простить»? Ну вот как вы себе это представляете?

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

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

> хотите иного поведения — напишите соотвествующий пропозал. Здравый смысл, он, знаете ли, у всех разный, потому в качестве ориентира ну никак не годится…

Если посмотреть, например, на трекер GCC, то там будет несколько сотен тикетов на тему «какого [...] оно взорвалось у меня в руках». Этого должно быть достаточно, чтобы увидеть проблему.
А конечные клиенты компилятора в лице прикладных программистов, скорее всего, не в состоянии написать ни один «пропозал» такого уровня, чтобы его стали рассматривать. Это не их дело и не их специализация, и не причина их высокомерно отвергать.

Именно поэтому Вы можете смело предлагать подходы в виде

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

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

Ваша программа вызвала UB. Да, бывает. Да, сочуствую. Да, могу предложить несколько способов: там UBSAN, ASAN и прочие всякие PVS-Studio. Про фаззинг не забудьте — тоже полезное дело.

А вот истерики на тему «у меня всё хорошо было, просто злой компилятор мою нежную душу не понял» — бросьте, неконструктивно это.

Если посмотреть, например, на трекер GCC, то там будет несколько сотен тикетов на тему «какого [...] оно взорвалось у меня в руках»
Несколько сотен тикетов, когда «оно взорвалось у меня в руках» вы увидите, а сотни тысяч мест, где оно не взорвалось и выкинутый код был реально не нужен — нет, так что это не показатель.

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

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

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

Вы можете жаловаться на «несправедливые законы» у курилке или даже в газете годами — но это ничего не изменит. Для того, чтобы закон изменился нужно, чтобы дума (или иной законодательный орган) его изменил. Так же и с C++: вы можете истерить до посинения, до пены из рта, но пока ваше предложение не пройдёт через комитет по стандартизации или, хотя бы, через разработчика компилятора, готового его отстаивать — толку не будет.

Пока никаких подобных поползновений я не увидел — вижу только лишь голые «наезды» и попытку переложить с больной головы на здоровую.

Многие люди много раз написали о Undefined behavior (UB).
В этой статье UB содержится в строчке:


return Do();

вместо которой должно быть, например:


return Do ? Do() : 0;

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

Надо было что-то более безобидное, чем rm -rf / — вдруг «кто-то» попробует не дочитав…
Sign up to leave a comment.