В GCC 6 появилась новая функция оптимизатора : она предполагает, что this
она всегда не равна нулю, и оптимизирует на основе этого.
Распространение диапазона значений теперь предполагает, что указатель this функций-членов C ++ не равен NULL. Это исключает обычные проверки нулевого указателя, но также нарушает некоторые несоответствующие кодовые базы (такие как Qt-5, Chromium, KDevelop) . В качестве временного обходного пути можно использовать -fno-delete-null-pointer-checks. Неправильный код можно определить с помощью -fsanitize = undefined.
Документ изменений явно называет это опасным, поскольку он нарушает удивительное количество часто используемого кода.
Почему это новое предположение нарушает практический код C ++? Существуют ли конкретные модели, в которых небрежные или неосведомленные программисты полагаются на это конкретное неопределенное поведение? Я не могу представить, чтобы кто-то писал, if (this == NULL)
потому что это так неестественно.
источник
this
передается как неявный параметр, поэтому они начинают использовать его так же, как если бы это был явный параметр. Это не. Когда вы разыменовываете null this, вы вызываете UB так же, как если бы вы разыменовывали любой другой нулевой указатель. Это все, что нужно. Если вы хотите передать nullptr, используйте явный параметр DUH . Он не будет медленнее, не будет более громоздким, и код, имеющий такой API, в любом случае глубоко во внутреннем пространстве, поэтому имеет очень ограниченную область применения. Конец истории, я думаю.Ответы:
Я предполагаю, что на вопрос, на который нужно ответить, почему люди с благими намерениями в первую очередь выписывают чеки.
Наиболее распространенный случай, вероятно, если у вас есть класс, который является частью естественного рекурсивного вызова.
Если у тебя есть:
в C вы можете написать:
В C ++ приятно сделать это функцией-членом:
В начале C ++ (до стандартизации) подчеркивалось, что функции-члены являются синтаксическим сахаром для функции, в которой
this
параметр является неявным. Код был написан на C ++, преобразован в эквивалентный C и скомпилирован. Были даже явные примеры того, что сравнениеthis
со значением null имеет смысл, и оригинальный компилятор Cfront также воспользовался этим. Исходя из фона C, очевидный выбор для проверки:Примечание: Бьярн Страуструп даже упоминает, что правила
this
изменились за эти годы здесьИ это работало на многих компиляторах много лет. Когда произошла стандартизация, это изменилось. И совсем недавно компиляторы начали использовать вызов функции-члена, где
this
бытие былоnullptr
неопределенным поведением, что означает, что это условие всегдаfalse
, и компилятор может его опустить.Это означает, что для любого обхода этого дерева вам необходимо:
Сделайте все проверки перед звонком
traverse_in_order
Это означает также проверку на КАЖДОМ сайте вызова, если у вас может быть нулевой корень.
Не используйте функцию-член
Это означает, что вы пишете старый код в стиле C (возможно, как статический метод) и вызываете его явно с объектом в качестве параметра. например. Вы вернулись к написанию,
Node::traverse_in_order(node);
а неnode->traverse_in_order();
на сайте вызова.Я полагаю, что самый простой / лучший способ исправить этот конкретный пример способом, который соответствует стандартам, - это на самом деле использовать сторожевой узел, а не
nullptr
.Ни один из первых двух вариантов не кажется привлекательным, и хотя код может сойти с рук, они написали плохой код,
this == nullptr
вместо того чтобы использовать правильное исправление.Я предполагаю, что так развивались некоторые из этих кодовых баз, чтобы иметь
this == nullptr
в них проверки.источник
1 == 0
быть неопределенное поведение? Это простоfalse
.this == nullptr
идиома - неопределенное поведение, потому что вы вызывали функцию-член для объекта nullptr до этого, который не определен. И компилятор может свободно пропустить проверкуthis
когда-либо можно будет обнулять. Я предполагаю, что, возможно, это является преимуществом изучения C ++ в эпоху, когда существует SO, чтобы укоренить опасности UB в моем мозгу и отговорить меня от таких странных хаков, как этот.Это происходит потому, что «практический» код был разбит и изначально включал неопределенное поведение. Нет никакой причины использовать нуль
this
, кроме как в качестве микрооптимизации, обычно очень преждевременной.Это опасная практика, поскольку корректировка указателей из-за обхода иерархии классов может превратить ноль
this
в ненулевой. Таким образом, по крайней мере, класс, чьи методы должны работать с нулем,this
должен быть конечным классом без базового класса: он не может быть производным от чего-либо и не может быть производным от него. Мы быстро отходим от практического к уродливой земле .На практике код не должен быть безобразным:
Если дерево пустое, то есть нулевое
Node* root
, вы не должны вызывать нестатические методы на нем. Период. Прекрасно иметь C-подобный древовидный код, который принимает указатель экземпляра по явному параметру.Аргумент здесь, кажется, сводится к тому, что каким-то образом нужно писать нестатические методы для объектов, которые можно вызывать из нулевого указателя экземпляра. Там нет такой необходимости. Способ написания такого кода на языке C-объектами все еще намного приятнее в мире C ++, потому что он может быть как минимум безопасным по типу. По сути, ноль
this
- это такая микрооптимизация, с такой узкой областью использования, что запретить ее - ИМХО совершенно нормально. Ни один публичный API не должен зависеть от нуляthis
.источник
this
проверки отбираются различными статическими анализаторами кода, так что это не так , как будто кто -то должен вручную охотиться их всех. Патч будет, вероятно, пара сотен строк тривиальных изменений.this
разыменование - это мгновенный сбой. Эти проблемы будут обнаружены очень быстро, даже если никто не захочет запускать статический анализатор над кодом. C / C ++ следует мантре «плати только за функции, которые ты используешь». Если вы хотите проверять, вы должны четко указывать о них, а это означает, что вы не должны их делатьthis
, когда уже слишком поздно, поскольку компилятор предполагает, чтоthis
он не равен нулю. В противном случае придется проверятьthis
, и для 99,9999% кода такие проверки являются пустой тратой времени.Документ не называет это опасным. Он также не утверждает, что нарушает удивительный объем кода . Он просто указывает на несколько популярных баз кода, которые, как он утверждает, как известно, основаны на этом неопределенном поведении, и могут сломаться из-за изменения, если не использовать опцию обходного решения.
Если практический код C ++ опирается на неопределенное поведение, то изменения этого неопределенного поведения могут нарушить его. Вот почему следует избегать UB, даже если программа, полагающаяся на него, работает должным образом.
Я не знаю , если это широкое распространение анти -pattern, но несведущий программист может думать , что они могут исправить свою программу от сбоев, выполнив:
Когда фактическая ошибка разыменовывает нулевой указатель где-то еще.
Я уверен, что если программист недостаточно информирован, он сможет предложить более продвинутые (анти) -шаблоны, которые полагаются на этот UB.
Я могу.
источник
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Например, удобный и удобный для чтения журнал последовательности событий, о которых отладчик не может вам легко рассказать. Получайте удовольствие отлаживая это сейчас, не тратя часов на размещение проверок везде, где есть неожиданный случайный ноль в большом наборе данных, в коде, который вы не написали ... И правило UB об этом было сделано позже, после создания C ++. Раньше был действительным.-fsanitize=null
.-fsanitize=null
регистрировать ошибки на SD / MMC-карте на контактах 5,6,10,11 с помощью SPI? Это не универсальное решение. Некоторые утверждают, что доступ к нулевому объекту противоречит объектно-ориентированным принципам, однако в некоторых языках ООП есть нулевой объект, с которым можно работать, поэтому это не универсальное правило ООП. 1/2Некоторые из «практических» (забавный способ записать «глючный») код, который был сломан, выглядели так:
и он забыл учитывать тот факт, что
p->bar()
иногда возвращает нулевой указатель, что означает, что разыменование его для вызоваbaz()
не определено.Не весь взломанный код содержал явные
if (this == nullptr)
илиif (!p) return;
проверки. Некоторые случаи были просто функциями , которые не получить доступ к любым переменным - членам, и так появились на работу OK. Например:В этом коде при вызове
func<DummyImpl*>(DummyImpl*)
с нулевым указателем происходит «концептуальное» разыменование указателя на вызываемый объектp->DummyImpl::valid()
, но на самом деле функция-член просто возвращаетсяfalse
без доступа*this
. Этоreturn false
может быть встроено, и поэтому на практике указатель вообще не нужен. Таким образом, с некоторыми компиляторами все работает нормально: нет никакого segfault для разыменования null, значениеp->valid()
false, поэтому код вызываетdo_something_else(p)
, который проверяет нулевые указатели, и поэтому ничего не делает. Никаких сбоев или неожиданного поведения не наблюдается.С GCC 6 вы по-прежнему получаете вызов
p->valid()
, но компилятор теперь выводит из этого выражения, что оноp
должно быть ненулевым (в противном случаеp->valid()
было бы неопределенное поведение), и запоминает эту информацию. Эта выведенная информация используется оптимизатором, так что если вызов todo_something_else(p)
встроен,if (p)
проверка теперь считается избыточной, поскольку компилятор запоминает, что она не равна нулю, и поэтому вставляет код в:Это теперь действительно разыменовывает нулевой указатель, и поэтому код, который раньше казался работающим, перестает работать.
В этом примере обнаружена ошибка
func
, которая должна была сначала проверяться на ноль (или вызывающие никогда не должны вызывать ее с нулем):Важно помнить, что большинство подобных оптимизаций не являются случаем того, что компилятор говорит: «ах, программист проверил этот указатель на ноль, я уберу его просто для раздражения». То, что происходит, - это то, что различные обычные оптимизации, такие как встраивание и распространение диапазона значений, объединяются, чтобы сделать эти проверки избыточными, потому что они идут после более ранней проверки или разыменования. Если компилятор знает, что указатель является ненулевым в точке A в функции, и указатель не изменяется до более поздней точки B в той же функции, то он знает, что он также ненулевой в B. Когда происходит встраивание точки A и B могут фактически быть фрагментами кода, которые изначально были в отдельных функциях, но теперь объединены в один фрагмент кода, и компилятор может применить свои знания о том, что указатель является ненулевым в большем количестве мест.
источник
this
?this
с нулем] " ?this
, то просто используйте-fsanitize=undefined
Стандарт C ++ нарушается в важных отношениях. К сожалению, вместо того, чтобы защищать пользователей от этих проблем, разработчики GCC решили использовать неопределенное поведение в качестве предлога для реализации предельных оптимизаций, даже когда им было ясно объяснено, насколько это опасно.
Здесь гораздо умнее человека, чем я объясняю в мельчайших подробностях. (Он говорит о Си, но там такая же ситуация).
Почему это вредно?
Простая перекомпиляция ранее работающего, безопасного кода с более новой версией компилятора может создать уязвимости безопасности . Хотя новое поведение может быть отключено с помощью флага, существующие make-файлы, очевидно, не имеют этого флага. И поскольку никаких предупреждений не выдается, разработчику не очевидно, что ранее разумное поведение изменилось.
В этом примере разработчик включил проверку на целочисленное переполнение, используя
assert
, что завершит работу программы, если указана неверная длина. Команда GCC удалила проверку на том основании, что целочисленное переполнение не определено, поэтому проверку можно удалить. Это привело к тому, что реальные случаи, когда эта кодовая база стала уязвимой после того, как проблема была исправлена.Прочитайте все это. Этого достаточно, чтобы заставить тебя плакать.
Хорошо, но как насчет этого?
В далеком прошлом была довольно распространенная идиома, которая звучала примерно так:
Итак, идиома: если
pObj
не ноль, вы используете дескриптор, который он содержит, в противном случае вы используете дескриптор по умолчанию. Это заключено вGetHandle
функцию.Хитрость заключается в том, что при вызове не виртуальной функции
this
указатель фактически не используется , поэтому нарушения доступа нет.Я до сих пор не понимаю
Существует много кода, который написан так. Если кто-то просто перекомпилирует его, не меняя строки, каждый вызов
DoThing(NULL)
- это ошибка, если вам повезет.Если вам не повезло, вызовы к сбоям ошибок становятся уязвимостями удаленного выполнения.
Это может произойти даже автоматически. У вас есть автоматизированная система сборки, верно? Обновление до последнего компилятора безвредно, верно? Но теперь это не так, если ваш компилятор - GCC.
Хорошо, так скажи им!
Им сказали. Они делают это в полном знании последствий.
но почему?
Кто может сказать? Может быть:
Или, может быть, что-то еще. Кто может сказать?
источник