Pull to refresh

Comments 44

Непонятно, почему вдруг появляется цикл


for (int i = 0; true; ++i)
    {
        std::cout << 1'000'000'000 * i << std::endl;
    }

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


for (int i = 0; i < 10'000'000'000; i += 1'000'000'000)
    {
        std::cout << i << std::endl;
    }

А потом, увидев, что 10 миллиардов в int не помещаются, заменяет условие на true.

Хорошее замечание, однако, вы говорите о другой, более сложной оптимизации (вроде такой: https://en.wikipedia.org/wiki/Induction_variable). Тут гораздо проще оптимизировать условие.

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

При такой оптимизации по крайней мере логично, что цикл не заканчивается. А иначе получается, что компилятор из вредности условие убирает - "раз UB позволяет, сделаю гадость".

И да, если в том же онлайн компиляторе посмотреть на asm, там есть прибавление миллиарда и нет умножения. Хотя кажется и какое-то условие выхода из цикла есть.

Суть то в том, что условие заменилось и всё:)
Думаю, что можно произвести бесчеловечный эксперимент: отключить оптимизацию Induction variable и увидеть тот же результат.

Посмотрел что за asm генерится при этом. И ага получается, что пока условие i < 3 компилятор может просчитать где надо остановиться. А вот когда уже i < 4 и так далее, все ломается и компилятор не кладет в выхлоп проверку на конец цикла.
Но что меня заинтересовало это то, что если проделать тоже самое с исходником на C (заменив вывод на printf("%d\n", i) конечно), то все норм. Выдается тот же варнинг, но компилятор просчитывает какое значение получится в финале после N итераций (с учетом переполнений) и сравнивает с ним.

Пример RNCE + DCE дичь. Какое-то нарушение причинно-следственных связей. RNCE оптимизация заменяет if (ptr == NULL) на if (false) из-за того, что выше есть разыменование, которое гарантирует, что либо точно ptr != NULL, либо будет исключение до конструкции if. А затем применяется DCE, который удаляет первопричину применения оптимизации RNCE. Какого? Если будет удалена первопричина применения RNCE, то она не может быть применена, а следовательно у DCE нет предпосылок применяться в таком виде.

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

Вы забываете, что речь идёт о языке, в котором встречаются вещи и страннее. Такой кейс действительно возможен, более того, похожая ошибка когда-то была в ядре Linux для Red Hat (подробнее можно почитать тут: https://lwn.net/Articles/342330/).

UFO just landed and posted this here

Спасибо за уточнение. Нулевой адрес, NULL и nullptr - это разные сущности. В комментарии выше я имел в виду нулевой адрес. Однако, справедливости ради, вы можете привести пример стандартной библиотеки, в которой макрос NULL определён не нулём?

Ну так там оптимизация многопроходная. И когда доходит дело до DCE, на изначальный код оптимизатор уже не смотрит. А разыменование гарантирует, что точно ptr != NULL, никаких "либо". ptr == NULL это UB, и его можно не рассматривать.

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

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

О каком семантическом заблуждении я? Излагать буду максимально подробно. Автор (масса авторов и комментаторов по всему интернету, к сожалению) в обсуждаемых терминах Стандарта “undefinded behaviour”, ”unspecified behaviour”, “implementation-defined behaviour” привязывает первое слово к последнему как характеристику последнего. В результате мы получаем чушь в форме “код, содержащий UB”. Извините, но B (поведение) скомпилированного кода очень даже определено — архитектурой целевой платформы и средой исполнения.Код не может содержать какого-то там неопределённого поведения. Или неуточнённого. Или реализацией-определямого. Он всегда будет исполняться так, как определено, повторюсь, архитектурой целевой платформы и средой исполнения.

Пример:

#include <inttypes.h>
#include <stdio.h>

static uint8_t a[3] = { 1, 0, 1 };

int main(void)
{
        printf("%hd/%hd\n", *(uint16_t *)a, *(uint16_t *)(a + 1));

        return 0;
}

Эта смешная программка (“содержащая UB”, выражаясь в стиле автора) на AMD64 всегда, будучи хоть миллиард раз запущена, выведет 1/256. Очень даже определённое поведение.

Эта же смешная программка на ARM точно так же всегда выведет — ахтунг! начинается интересное — либо 1/256, либо 256/1, и зависит это от среды исполнения (для тех, кто не знает — порядок байтов в многобайтовых словах, так называемый endian, на ARM настраивается при старте, и может быть как little, так и big), Но в одной и той же среде результат при миллиардах запусков будет один, то есть поведение вполне себе определённое.

И эта же смешная программка на PDP-11/VAX всегда, сколько ни запускай, фолтнется с bus error. И это тоже вполне определено, нельзя там выдёргивать слово по нечётному адресу.

То есть, поведение программки всегда детерминировано, полностью определено. Но позвольте, она же “содержит UB”, как же так?

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

Попробуем теперь переосмыслить и выразить простым русским языком значения этих терминов:

unspecified behaviourСтандарт определяет, что параметры функции конечно же будут вычислены, и это точно произойдёт до вызова функции, но Стандарт не уточняет порядок вычисления этих параметров в случае, когда их более одного.

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

undefined behaviourСтандарт не определяет, каким будет результат использования той или иной языковой конструкции в исполняемом коде на целевой платформе в целевой среде исполнения.

Чувствуете же разницу, друзья-коллеги? Не “Поведение неопределённое”, а “Стандарт никоим образом не определяет, каким должно быть поведение” при целочисленном делении на 0 — на PDP-11/VAX всегда и определённо трапнется, на MIPS молча и так же всегда и c чётко определённым результатом поедет дальше.

Ничего феноменального в UB нет, надо лишь вспомнить чуть-чуть историю языка C. Его не зря всегда сравнивали с высокоуровневым ассемблером — это язык очень низкого уровня, почти абстрактный ассемблер и есть. Множество архитектур, на которые он был в то или иное время перенесён, имеют существенные различия. И эти различия не особо напрягали как отцов, так и многочисленных “портировщиков” UNIX (напоминаю — C родился для создания переносимой UNIX) на другие платформы, принципиальные отличия в архитектуре решались условной компиляцией и чистым ассемблером в совсем уж железо-зависимых частях.

Но потом появился Стандарт. И члены рабочей группы прекрасно понимали, что решение проблем разности архитектур при таком многообразии архитектур — mission impossible. Для упрощения проблем реализаций сначала определили кучу implemetation-defined behaviour, а потом, абстрагируясь всё выше, и undefined behaviour.

А потом “в тусовку” пришли дети Фортрана и Бейсика, решившие, что они тоже хотят писать на C всё те же свои математические, физические, экономические и прочие очень далёкие от “железа” программы. Им многого не хватало из привычных им сред, и они стали прессовать Комитет. И Комитет повёлся. В язык стали вводить конструкции, которые стали превращать очень низкоуровневый C во всё более высокоуровневый язык (взять хотя бы те же VLA). И ситуация дошла “до ручки”. Одно из самых свежих нововведений — сравнение указателей, не относящихся к одной и той же области storage— UB. У кучи разработчиков системного (очень низкого) уровня подорвались пердаки. Мегасрачи на reddit. Но всё же понятно при всей своей непонятности: тому, кто не знает, чем стэнфордская архитектура отличается от гарвардской архитектуры, тяжело понять, почему нельзя сравнивать указатели на код с указателями на данные. Но как быть в системах, где нет защиты памяти, и можно сгенерить код в области данных, и передать ему управление (привет вирусописателям)? А как быть на гарвардской архитектуре, где код лежит в 16-разрядной памяти, а данные — в 32-разрядной?

Комитет пошёл на поводу у людей, которые о low-level программировании — ни ухом, ни рылом, какими бы доками они ни было в квантовой физике и макроэкономике. Комитет смирился с идеей превратить низкоуровневый C в высокоуровневый C-хрен_знает_что. Язык, на котором можно было (и неоднократно реализовано) делать операционные системы, превращается в сверхвысокоуровневое, не имеющее никакого отображения на железо, нечто, на котором можно писать очередное 1С, но ОС или хотя бы драйвер — нет, ребята, пишите на ассемблере.

А потом C++ посредством механизма наследования (какая издёвка над столпами ООП) получил всё это дерьмо на свой обеденный стол. Держитесь, ребята. Или, осознавая существенные отличия архитектур, переносимо операционки писать, или разргребать, подобно дерьму, существенные отличия архитектур в угоду Васе, который не может понять, почему нельзя просто поделить икс на игрек...

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

Извините, но B (поведение) скомпилированного кода очень даже определено — архитектурой целевой платформы и средой исполнения.

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

Например, следующий код ведёт себя по-разному в gcc 6.3 и gcc 6.4 (2017 год), но только если включить оптимизации -O2: в первом случае считает указатели p и q и их числовые представления всегда разными, а во втором — указатели разными, а их числовые представления одинаковыми.

#include <stdio.h>
#include <inttypes.h>
int main() {
    int foo[3];
    int bar[3];
    int *p = foo + 3;
    int *q = bar;
    printf("%p %p are equal? %d\n",
             p, q,
             p == q);
    printf("0x%" PRIxPTR " 0x%" PRIxPTR " are equal? %d\n",
            (intptr_t)p,  (intptr_t)q,
            (intptr_t)p == (intptr_t)q);
}

А совсем классический пример — это, конечно, удаление проверки на NULL после неиспользуемого разыменования, ещё в 2007 поехало. Так что с современными компиляторами договор "вы просто генерируете понятно какой код в зависимости от платформы" уже не работает, фраза "это по стандарту UB, делаем что хотим" используется как универсальный повод делать довольно суровые оптимизации.

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

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

Имея же в виду "программу" как исходный код автор вполне себе прав утверждая что "программа" содержит UB.

В качестве примера: утверждая что

Эта смешная программка (“содержащая UB”, выражаясь в стиле автора) на AMD64 всегда, будучи хоть миллиард раз запущена, выведет 1/256.

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

То есть, поведение программки всегда детерминировано, полностью определено.

Интересно, а чем определяется поведение вот такой программы: https://godbolt.org/g/o4HxtU ?

но B (поведение) скомпилированного кода очень даже определено

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

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

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

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

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

Извините, но B (поведение) скомпилированного кода очень даже определено — архитектурой целевой платформы и средой исполнения.

Да, определено, но, беря тот же пример с залезанием за пределы массива, мы, запуская бинарь, не можем предсказать, что он выведет к примеру. В моем понимании именно это и есть неопределенное поведение (Undefined Behavior).
А то, что вы говорите это как раз Unspecified Behavior.
Обращаясь к примеру с порядком вычисления аргументов функции, мы на момент написания кода не можем знать каким он будет. Собирая разными компиляторами мы будем получать разный порядок. Но в уже скомпилированном бинаре порядок будет определен и сколько его не запускай, он не изменится.

Некорректно написанная программа (Ill-formed program) — программа, которая нарушает либо синтаксические, либо семантические правила (либо и те, и другие). Она не должна компилироваться.

Также бывает "Ill-formed, no diagnostic required" — программа некорректна, но имеет полное право компилироваться без предупреждений. Что произойдёт при попытке запустить — не определено. Может выдать верный ответ, может выдать неверный ответ, может не запуститься, может вести себя по-разному в зависимости от настроек ОС. Типичный пример — static initialization order fiasco.

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

This diagnostics is produced. I'd like to point out that clang behaves similarly (albeit this is not really relevant when we talk about the correctness).

UB - стандарт говорит компилятору делай как хочешь.

Тем больше UB, тем больше свободы разработчикам компиляторов.

Тем больше свободы, тем больше кто в лес, кто по дрова.

В итоге ты смотришь на код и НЕ ПОНИМАЕШЬ что же в итоге выполнится.

В общем, стандарт плохой и из-за этого язык стал "плохим".

И комитет ещё не спит, сыпет новыми версиями стандартов с новыми UB.

Можно всю жизнь учить c++ и все равно не знать что же происходит. Особенно в чужом коде :'(

UFO just landed and posted this here

А как делать тогда? Посмотрите на самый первый пример.

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

А если там будет три параметра порядок вычисления точно будет линейным? А кто его знает что там в оптимизаторе закодено!

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

И вот ты читаешь код и НЕ ПОНИМАЕШЬ как он работает. Надо лезть в документацию и искать там описание этого UB. И хорошо если не придется еще и документацию к платформе искать и вычитывать там поведение которое может отличаться от значении в регистрах.

Во всех остальных "нормальных" языках ты читаешь код и ПОНИМАЕШЬ как он будет выполняться.

Любой кто читает этот код вполне логично подумает что аргументы будут вычисляться слева направо

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

А как делать тогда?

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

Лично для меня почти все примеры с UB - это какой-то реально сомнительный код.

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

Порядок вычисления аргументов — это unspecified behaviour, а не implementation-defined behaviour, т.е. он не должен и не может быть описан в документации к компилятору.

UFO just landed and posted this here

Могу назвать пример, где UB жил много лет. GetSafeHWND().

Хотя это, конечно, не то, о чём спрашивали, избавиться от него элементарно (сделать GetSafeHWND не методом, а функцией с одним аргументом).

Посмотрите на самый первый пример.

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

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

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

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

UPD: например, рассмотрим такой код:

#include <iostream>
#include <string_view>
std::string_view foo() {
    return "hello world\n";
}
int main() {
    std::string_view f = foo().substr(1);
    std::cout << f << "\n";
}

В нём UB отсутствует. foo и main могут быть вообще в разных компонентах программы. Дальше оказалось, что foo иногда надо возвращать не статическую строку, а что-то динамическое. Поменяли возвращаемый тип на std::string . Казалось бы, сделали безопаснее: раньше возвращали указатель непонятно куда, теперь возвращаем обычный RAII-шный тип. Но на самом деле получили UB в main, который находится в другом углу программы. Причём предупреждений от компилятора GCC ноль, да и на коротких строках никаких последствий не будет.

UFO just landed and posted this here

Всё верно.

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

UFO just landed and posted this here

Банальный поиск по файлам по имени функции с включенными опциями поиска слова целиком и с учётом регистра позволяет найти практически все места.

В конкретном случае проблем никаких.

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

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

и рассказывает проджект менеджерам что это все потому что это "нереалистичное требование".

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

Работающий на конкретном компиляторе и конкретных данных — да. Но так чтобы можно было хотя бы версию компилятора обновить и ничего не сломать — нет. Да у людей проекты начинают ломаться даже если перед std::sort случайным образом элементы перемешать, потому что они рассчитывают на стабильность сортировки или что-то близкое к ней. Конечно так не надо делать, в стандарте этого не гарантируется, но по факту-то так почему-то люди продолжают регулярно делать. Что в мелких компаниях, что размером с Google.

Меня огорчает то что в других языках это можно как-то отладить и отследить: если в программе баг, то до момента появления бага программа работает корректно, и после появления бага не связанные с багом данные остаются корректные. В C++ нет ни одной из этих гарантий ни в теории, ни на практике: если обратились к висячей ссылке — то может побиться и куча, и стек, и в других потоках тоже. Если у вас сервер обрабатывает несколько клиентов и при обработке одного у вас произошло UB — лучше от греха подальше прибить весь процесс, а не только обработчик конкретного клиента.

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

Хороший идей у меня нет. Все что есть (включая текущую) — чем-то серьёзно плохи. То количество кода сильно увеличивается, то ещё нетривиальные UB добавляются, то ещё что-то. Да даже когда вводили move semantics в C++11 — это делали наиболее изящным способом, мне кажется, но только с учётом существующих ограничений. Так-то проблем всё равно куча.

Но если первой отработает RNCE, то код станет таким (оптимизатор видит, что ptr проверяется на NULL уже после разыменования, соответственно, проверка бессмысленна):

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

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

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

Гипотетический пример:

#include <iostream>
int f(int x) {
  if (x < 0 || x >= 10) {
    return -1;
  }
  return x;
}
int main() {
  for (int i = 0; i < 10; ++i) {
    std::cout << f(i) << "\n";
  }
}

Здесь в общем случае проверка в f не лишняя. Но если заинлайнить f в цикл, то становится очевидно лишней. Кажется, что предупреждение выдавать всё-таки не следует.

Здесь в общем случае проверка в f не лишняя. Но если заинлайнить f в цикл, то становится очевидно лишней. Кажется, что предупреждение выдавать всё-таки не следует.

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

Технически не вижу проблемы пометить сгенерированный if соответствующим флагом. Или может быть я что-то упускаю?

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

#include <iostream>
int len(const char *s) {
  if (!s) {
    return 0;
  }
  int l = 0;
  while (s[l]) l++;
  return l;
}
int get_first(const char *s) {
  int f = static_cast<unsigned char>(s[0]);
  if (len(s) == 0) {
    f = -1;
  }
  return f;
}
int main() {
  std::cout << get_first("hello") << "\n";
  std::cout << get_first(nullptr) << "\n";
}

Здесь компилятор может заинлайнить len (которая используется в куче разных мест) в get_first. После этого увидеть, что из-за строчки `f = s[0]` можно предполагать, что s — не nullptr, и убрать заинлайненную проверку `if (!s)`. И не выдать предупреждение, ведь этот иф "написал компилятор". Но в данном случае предупреждение как раз хочется: get_first думает что случай "пустая строка" разобран, а на самом деле разобран не до конца. Конечно, в данном случае легко увидеть вызов от nullptr, но его можно делать из другой единицы трансляции, сторонней библиотеки, или вообще замаскировать.

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

UFO just landed and posted this here

Да, поэтому и хотим предупреждение — некорректный код.

Я это к чему: вот перед вами два примера кода. В первом код корректен и предупреждений не ожидается. Но после инлайнинга f в main оказывается, что if лишний: условие всегда неверно. Предупреждение выдавать всё ещё не хочется. Вывод: нельзя выдавать предупреждения на заинлайненых if, если условие всегда неверно.

В другом код некорректен и предупреждения ожидаются. Их можно выдать только если len заинлайнен в get_first, причём предупреждение возникнет только после анализа заинлайненого if (!s): условие всегда неверно из предположения "никто не будет разыменовывать нулевой указатель". Вывод: если заинлайнен if с всегда неверным условием, нужно выдавать предупреждение.

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

Более того, может у нас вообще в проекте базе есть договорённость "нулевой указатель не является корректной строкой". Тогда код остаётся абсолютно такой же,get_first корректен, len содержит лишнюю проверку, а некорректен уже main, вызывающий get_first(nullptr). Компилятору это понять без шансов.

UFO just landed and posted this here

Функции с циклами не инлайнятся, но допустим, что да.

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

Нет, мои требования не противоречивые, я не требую ловить и этот случай. Действительно, в данном случае предупреждения не будет (и не надо), его не будет и при нынешнем подходе, зато в каких-то других случаях моё предупреждение сработает. То есть хуже не будет, улучшение от такого подхода будет, но в некоторых случаях инлайнинга оно не сработает. В данном случае единственное, что может попытаться поймать компилятор, это вызов nullptr в main(). Он может сам себе добавить constraint при оптимизации, что параметр s!=0 в get_first()и таким образом отловить вызов get_first(nullptr)в main().

По поводу того, может ли понять компилятор — в тех случаях где он не может, хорошо бы иметь возможность давать ему подсказки. Например иметь конструкцию языка, чтобы программист мог подсказать, этот параметр не должен быть нулём. Типа contraint s!=0, тогда компилятор мог бы сориентироваться лучше, оптимизировать ему или нет и даже посоветовать добавить его в данном случае. По-моему в новом стандарте это возможно (я не разбирался ещё в деталях). Теоретически компилятор может даже требовать добавить такой contraint явно.

Функции с циклами не инлайнятся, но допустим, что да.

Не очень понял, вот буквально в этом же примере инлайнятся:get_first превратилась в один if. Или вы про другое? Там только версия gcc 4.1.2 не инлайнит, все более новые (начиная с 4.4.7) инлайнят.

А, ну это значит я отстал от жизни :) Раньше не инлайнились.

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

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

Sign up to leave a comment.