Философия, лежащая в основе неопределенного поведения

59

Спецификации C \ C ++ оставляют большое количество вариантов поведения, открытых для компиляторов, чтобы реализовать их по-своему. Есть ряд вопросов, которые постоянно задают здесь о том же самом, и у нас есть несколько отличных постов об этом:

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

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

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

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

Alok Save
источник
7
кто нибудь слышал о детерминированном компьютере?
Сова
1
как указывает отличный ответ от litb programmers.stackexchange.com/a/99741/192238 , заголовок и текст этого вопроса кажутся немного несоответствующими: «поведение, открытое для компиляторов для реализации по-своему», обычно называют определяемыми реализацией , Конечно, фактический UB может быть определен автором реализации, но чаще всего они не беспокоят (и все это оптимизируют и т. д.)
underscore_d
Нечто похожее на это программное обеспечениеengineering.stackexchange.com
questions/398703/…

Ответы:

49

Во-первых, я отмечу, что хотя я упоминаю здесь только «C», то же самое в равной степени относится и к C ++.

Комментарий, в котором упоминался Годель, был частично (но только частично) точным.

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

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

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

Рискуя (опять же) быть обвиненным в высокомерии, я бы охарактеризовал стандарт C как управляемый (частично) двумя основными идеями:

  1. Язык должен поддерживать как можно более широкое разнообразие аппаратного обеспечения (в идеале, все «вменяемое» оборудование до некоторого разумного нижнего предела).
  2. Язык должен поддерживать написание как можно более широкого спектра программного обеспечения для данной среды.

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

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

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

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

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

Эти различия в намерениях определяют большинство различий между C и чем-то вроде Java или системами на основе CLI от Microsoft. Последние довольно явно ограничены работой с гораздо более ограниченным набором оборудования или требованием программного обеспечения для эмуляции более конкретного оборудования, на которое они нацелены. Они также специально намерены предотвратить любые прямые манипуляции с оборудованием, вместо этого требуя, чтобы вы использовали что-то вроде JNI или P / Invoke (и код, написанный на чем-то вроде C), чтобы даже предпринять такую ​​попытку.

Возвращаясь к теоремам Годеля на минуту, мы можем провести нечто вроде параллели: Java и CLI выбрали «внутренне согласованную» альтернативу, а C - «полную» альтернативу. Конечно, это очень грубая аналогия - я сомневаюсь , что кто -то пытается - х формальное доказательство либо внутренней согласованности или полноты в любом случае. Тем не менее, общее понятие довольно близко соответствует выбору, который они приняли.

Джерри Гроб
источник
25
Я думаю, что теоремы Годеля - красная сельдь. Они имеют дело с проверкой системы по ее собственным аксиомам, что здесь не так: C не нужно указывать в C. Вполне возможно иметь полностью определенный язык (рассмотрим машину Тьюринга).
пул
9
Извините, но я боюсь, что вы совершенно не поняли теоремы Годеля. Они имеют дело с невозможностью доказать все истинные утверждения в последовательной системе логики; в терминах вычислений теорема о неполноте аналогична утверждению, что существуют проблемы, которые не могут быть решены ни одной программой - проблемы аналогичны истинным утверждениям, программам для доказательств и модели вычислений для логической системы. Это никак не связано с неопределенным поведением. Смотрите объяснение аналогии здесь: scottaaronson.com/blog/?p=710 .
Алекс тен Бринк
5
Я должен отметить, что машина Von Neumann не требуется для реализации на Си. Вполне возможно (и даже не очень сложно) разработать реализацию C для архитектуры Гарварда (и я не удивлюсь, увидев множество таких реализаций во встроенных системах)
bdonlan
1
К сожалению, современная философия компилятора Си выводит UB на совершенно новый уровень. Даже в тех случаях, когда программа была подготовлена ​​для того, чтобы справиться почти со всеми правдоподобными «естественными» последствиями определенной формы неопределенного поведения, и те, с которыми она не могла справиться, по крайней мере, были бы узнаваемы (например, переполнение целых чисел в ловушке), новая философия одобряет обойдя любой код, который не может быть выполнен, если только не возникнет UB, превратив код, который будет вести себя корректно при любой реализации, в код, который «более эффективен», но просто неверен.
суперкат
20

C обоснование объясняет

Термины «неопределенное поведение», «неопределенное поведение» и «поведение, определяемое реализацией» используются для классификации результатов написания программ, свойства которых Стандарт не описывает или не может полностью описать. Цель принятия этой категоризации состоит в том, чтобы обеспечить определенное разнообразие среди реализаций, которое позволяет качеству реализации быть активной силой на рынке, а также разрешить некоторые популярные расширения , не удаляя кэш соответствия стандарта. В Приложении F к Стандарту перечислены те виды поведения, которые подпадают под одну из этих трех категорий.

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

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

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

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

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

И в 1.7 это отмечает

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

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

Таким образом, эта маленькая грязная программа, которая прекрасно работает на GCC, все еще соответствует !

Йоханнес Шауб - Литб
источник
15

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

Другое неопределенное поведение просто отражает реальность. Одним из примеров является сдвиг битов с числом больше, чем тип. Это на самом деле отличается между аппаратными поколениями одной семьи. Если у вас есть 16-битное приложение, один и тот же двоичный файл даст разные результаты для 80286 и 80386. Так что языковой стандарт говорит, что мы не знаем!

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

Бо Перссон
источник
+1 для второго абзаца, который показывает что-то, что было бы неудобно указывать как поведение, определяемое реализацией.
Дэвид Торнли
3
Сдвиг битов - всего лишь пример принятия неопределенного поведения компилятора и использования аппаратных возможностей. Было бы тривиально указать результат C для сдвига битов, когда число больше, чем тип, но дорого реализовать на некоторых аппаратных средствах.
Mattnz
7

В качестве одного примера, указатель доступа почти должен быть неопределенным и не обязательно только по соображениям производительности. Например, в некоторых системах загрузка определенных регистров с указателем вызовет аппаратное исключение. При обращении к SPARC неправильно выровненный объект памяти вызовет ошибку шины, но в x86 это будет «просто» медленно. На самом деле сложно определить поведение в этих случаях, поскольку базовое оборудование диктует, что произойдет, а C ++ переносим на многие типы оборудования.

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

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

Марк Б
источник
3
Язык C мог быть задан таким образом, чтобы он всегда использовал побитовое чтение в системах с ограничениями выравнивания, и чтобы он должен был обеспечивать ловушки исключений с четко определенным поведением для недопустимых обращений к адресу. Но, конечно, все это было бы невероятно дорого (по размеру кода, сложности и производительности) и не дало бы никакой пользы для правильного и корректного кода.
R ..
6

Просто: скорость и портативность. Если C ++ гарантирует, что вы получите исключение при отмене ссылки на недопустимый указатель, то он не будет переносимым на встроенное оборудование. Если бы C ++ гарантировал некоторые другие вещи, такие как всегда инициализируемые примитивы, то это было бы медленнее, а во время создания C ++ медленнее было действительно очень плохо.

DeadMG
источник
1
А? Как исключения связаны со встроенным оборудованием?
Мейсон Уилер
2
Исключения могут блокировать систему таким образом, что это очень плохо для встроенных систем, которые должны быстро реагировать. Есть ситуации, когда ложное чтение наносит гораздо меньший вред, чем замедленная система.
Мировой инженер
1
@ Мейсон: потому что оборудование должно перехватить неправильный доступ. Windows легко выдает нарушение доступа, а встраиваемому оборудованию без операционной системы делать что-либо, кроме смерти, сложнее.
DeadMG
3
Также помните, что не каждый ЦП имеет MMU для защиты от недопустимого доступа к оборудованию с самого начала. Если вы начинаете требовать, чтобы ваш язык проверял все обращения к указателям, то вам придется эмулировать MMU на ЦП без такового - и, таким образом, КАЖДЫЙ доступ к памяти становится чрезвычайно дорогим.
пушистый
4

C был изобретен на машине с 9-битными байтами и без единицы с плавающей запятой - предположим, что он предписал, чтобы байты были 9-битными, слова 18-битными, и что числа с плавающей запятой должны быть реализованы с использованием pre IEEE754 aritmatic?

Мартин Беккет
источник
5
Я подозреваю, что вы думаете о Unix - C изначально использовался на PDP-11, который на самом деле был довольно обычными современными стандартами. Я думаю, что основная идея остается тем не менее.
Джерри Коффин
@ Джерри - да, ты прав - я старею!
Мартин Беккет
Да, случается с лучшими из нас, я боюсь.
Джерри Коффин
4

Я не думаю, что первым обоснованием для UB было предоставление возможности компилятору оптимизировать, а просто возможность использовать очевидную реализацию для целей в то время, когда архитектуры имели большее разнообразие, чем сейчас (помните, если C был разработан на PDP-11, который имеет несколько знакомую архитектуру, первым портом был Honeywell 635, который гораздо менее знаком - адресация по словам, с использованием 36-битных слов, 6 или 9-битных байтов, 18-битных адресов ... ну, по крайней мере, он использовал 2-х дополнение). Но если интенсивная оптимизация не была целью, очевидная реализация не включает в себя добавление проверок во время выполнения для переполнения, счетчика сдвигов по размеру регистра, который псевдоним в выражениях, изменяющих несколько значений.

Еще одна вещь, принятая во внимание, была простота реализации Компилятор AC в то время выполнял несколько проходов, используя несколько процессов, потому что с одним обработчиком процесса все было бы невозможно (программа была бы слишком большой). Запрашиваемая тщательная проверка согласованности была исключена, особенно когда в ней участвовало несколько БЧ. (Для этого использовалась другая программа, кроме компиляторов Си, lint).

AProgrammer
источник
Интересно, что побудило меняющуюся философию UB от «Разрешить программистам использовать поведение, демонстрируемое их платформой» до «Найти оправдания, позволяющие компиляторам реализовывать совершенно дурацкое поведение»? Мне также интересно, насколько такие оптимизации приводят к улучшению размера кода после модификации кода для работы под новым компилятором? Я не удивлюсь, если во многих случаях единственный эффект добавления таких «оптимизаций» в компилятор - это заставить программистов писать код больше и медленнее, чтобы компилятор не сломал его.
суперкат
Это дрейф в POV. Люди стали меньше знать о машине, на которой работает их программа, они стали больше заботиться о переносимости, поэтому они избегали зависеть от неопределенного, неопределенного и определенного реализацией поведения. Оптимизаторы оказывали давление, чтобы получить наилучшие результаты в тестах, а это означает, что нужно использовать все снисхождения, оставленные спецификацией языков. Существует также тот факт, что Интернет - Usenet в то время, SE в настоящее время - юристы по языку, также склонны давать предвзятый взгляд на основную причину и поведение авторов компиляторов.
AProgrammer
1
Что я нахожу любопытным, так это заявления о том, что «С предполагает, что программисты никогда не будут вести себя неопределенным образом» - факт, который исторически не был правдивым. Правильным утверждением было бы то, что «C предполагал, что программисты не будут запускать поведение, не определенное стандартом, если не готовы справиться с естественными последствиями этого поведения для платформы . Учитывая, что C был разработан как язык системного программирования, большая часть его предназначения должен был позволить программистам делать специфичные для системы вещи, не определенные языковым стандартом; идея, что они никогда этого не сделают, абсурдна
суперкат
Программистам полезно прилагать дополнительные усилия для обеспечения переносимости в тех случаях, когда разные платформы по своей сути делают разные вещи , но разработчики компиляторов тратят время каждого, когда они устраняют поведение, которое программисты исторически могли разумно ожидать, чтобы быть общим для всех будущих компиляторов. Учитывая целые числа iи n, такие, что n < INT_BITSи i*(1<<n)не будут переполнены, я бы посчитал, i<<=n;что более понятным, чем i=(unsigned)i << n;; на многих платформах это будет быстрее и меньше, чем i*=(1<<N);. Что получается, когда компиляторы запрещают это?
суперкат
Хотя я думаю, что для стандарта было бы хорошо разрешить ловушки для многих вещей, которые он называет UB (например, целочисленное переполнение), и для этого есть веские причины не требовать, чтобы ловушки делали что-либо предсказуемое, но я думаю, что с любой точки зрения, которую можно себе представить, Стандарт был бы улучшен, если бы требовалось, чтобы большинство форм UB либо приводили к неопределенному значению, либо документировали тот факт, что они оставляли за собой право делать что-то еще, при этом не было абсолютно необходимости документировать, что это может быть за что-то еще. Компиляторы, которые делали все "UB", были бы законными, но, вероятно, не в
пользу
3

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

Дэвид Торнли
источник
Целочисленное сложение - интересный случай; Помимо возможности перехвата, который в некоторых случаях был бы полезен, но мог бы в других случаях вызывать случайное выполнение кода, существуют ситуации, когда компилятору было бы разумно делать выводы, основываясь на том факте, что переполнение целочисленного значения не указано для переноса. Например, компилятор, в котором int16 битов и сдвиги с расширенными знаками, дороги, может вычислять (uchar1*uchar2) >> 4с использованием сдвигов без расширений знаков. К сожалению, некоторые компиляторы распространяют выводы не только на результаты, но и на операнды.
суперкат
2

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

jmoreno
источник
Первоначально многие варианты поведения были оставлены неопределенными, чтобы учесть возможность того, что разные системы будут делать разные вещи, включая запуск аппаратной ловушки с помощью обработчика, который может или не может быть конфигурируемым (и может, если не настроен, вызывать произвольно непредсказуемое поведение). Например, требование, чтобы сдвиг влево отрицательного значения, а не ловушка, нарушало бы любой код, который был разработан для системы, в которой он работал и полагался на такое поведение. Короче говоря, они были оставлены неопределенными, чтобы не помешать разработчикам обеспечить поведение, которое они считали полезным .
суперкат
К сожалению, однако, это было искажено так, что даже код, который знает, что он работает на процессоре, который будет делать что-то полезное в конкретном случае, не может воспользоваться таким поведением, потому что компиляторы могут использовать тот факт, что стандарт C не не указывать поведение (хотя платформа будет), чтобы применить bizarro-world переписывает к коду.
суперкат
1

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

Тадеуш Копец
источник
или вы могли бы выделить все указатели как weak_ptrи обнулить все ссылки на указатель, который получает delete... о, подождите, мы приближаемся к сборке мусора: /
Matthieu M.
boost::weak_ptrРеализация является довольно хорошим шаблоном для этого шаблона использования. Вместо того, чтобы отслеживать и обнулять weak_ptrsвнешне, weak_ptrпросто вносит вклад в shared_ptrслабый счетчик, а слабый счет - это, в основном, пересчет к самому указателю. Таким образом, вы можете аннулировать shared_ptrбез необходимости немедленного удаления. Он не идеален (у вас все еще может быть много просроченных weak_ptrс сохранением базиса shared_countбез веской причины), но по крайней мере это быстро и эффективно.
пушистый
0

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

Это в основном дает вам два варианта:

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

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

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

Дэвид Шварц
источник
Было бы семантическое различие между высказыванием «Компилятору разрешено вести себя так, как будто X является истиной» и высказыванием «Любая программа, в которой X не соответствует истине, будет участвовать в неопределенном поведении», хотя, к сожалению, стандарты не проясняют различие. Во многих ситуациях, в том числе в вашем примере с псевдонимами, предыдущий оператор допускает многие оптимизации компилятора, которые в противном случае были бы невозможны; последний допускает еще несколько «оптимизаций», но многие из последних оптимизаций - это то, чего программисты не хотят.
суперкат
Например, если какой-то код устанавливает fooзначение 42, а затем вызывает метод, который использует незаконно измененный указатель для установки значения foo44, я вижу преимущество в том, что до следующей «законной» записи fooпопытки его чтения могут законно выведите 42 или 44, а выражение вроде foo+fooможет даже вывести 86, но я вижу гораздо меньшую выгоду, позволяя компилятору делать расширенные и даже ретроактивные выводы, изменяя неопределенное поведение, правдоподобные «естественные» поведения которого были бы все доброкачественными, в лицензию генерировать бессмысленный код.
суперкат
0

Исторически неопределенное поведение имело две основные цели:

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

  2. Чтобы учесть возможность того, что в отсутствие кода для явной обработки таких условий реализации могут иметь различные виды «естественных» поведений, которые в некоторых случаях были бы полезны.

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

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

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

стандарт C позволил бы компилятору сказать, что если q равно 46341 или больше, выражение q * q даст слишком большой результат, чтобы его можно было вместить, в intрезультате чего возникнет неопределенное поведение, и в результате компилятор будет иметь право предполагать, что не может произойти и, следовательно, не требуется увеличивать, *pесли это происходит. Если вызывающий код использует *pв качестве индикатора, который должен отбрасывать результаты вычислений, эффект оптимизации может состоять в том, чтобы взять код, который дал бы ощутимые результаты в системах, которые работают практически любым мыслимым образом с целочисленным переполнением (перехват может быть некрасиво, но, по крайней мере, было бы разумно), и превратило его в код, который может вести себя бессмысленно.

Supercat
источник
-6

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

ddyer
источник
7
ОП указала следующее: «Мой вопрос не о том, что такое неопределенное поведение, или оно действительно плохое. Я знаю об опасностях и большинстве соответствующих неопределенных цитат поведения из стандарта, поэтому, пожалуйста, воздержитесь от публикации ответов о том, насколько это плохо». «. Похоже, вы не читали вопрос.
Этьен де Мартель