В какой момент цикла целочисленное переполнение становится неопределенным?


86

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

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Эта программа содержит неопределенное поведение на моей платформе, потому aчто в третьем цикле произойдет переполнение.

Делает ли это поведение всей программы неопределенным или только после того, как действительно происходит переполнение ? Может ли компилятор потенциально работать, что a будет переполняться , поэтому он может объявить весь цикл неопределенного и не беспокоить , чтобы запустить printfs , даже если они все произойдут до перелива?

(Помечены C и C ++, хотя они разные, потому что меня бы интересовали ответы для обоих языков, если они разные.)


7
Интересно, может ли компилятор сработать, что aне используется (кроме вычислений), и просто удалитьa
4386427

12
Возможно, вам понравится My Little Optimizer: Undefined Behavior is Magic от CppCon в этом году. Все дело в том, какие оптимизации могут выполнять компиляторы на основе неопределенного поведения.
TartanLlama



Ответы:


108

Если вас интересует чисто теоретический ответ, стандарт C ++ допускает неопределенное поведение "путешествия во времени":

[intro.execution]/5: Соответствующая реализация, выполняющая правильно сформированную программу, должна производить такое же наблюдаемое поведение, как одно из возможных выполнений соответствующего экземпляра абстрактной машины с той же программой и теми же входными данными. Однако, если любое такое выполнение содержит неопределенную операцию, настоящий международный стандарт не налагает никаких требований на реализацию, выполняющую эту программу с этим вводом (даже в отношении операций, предшествующих первой неопределенной операции).

Таким образом, если ваша программа содержит неопределенное поведение, то поведение всей вашей программы не определено.


4
@KeithThompson: Но тогда сама sneeze()функция не определена ни для чего из этого класса Demon(подклассом которого является носовая разновидность), что в любом случае делает все это круговым.
Себастьян Ленартович

1
Но printf может не вернуться, поэтому первые два раунда определены, потому что до тех пор, пока не будет выполнено, не ясно, будет ли когда-либо UB. См. Stackoverflow.com/questions/23153445/…
usr

1
Вот почему компилятор технически имеет право выдавать «nop» для ядра Linux (потому что код начальной загрузки зависит от поведения undefined): blog.regehr.org/archives/761
Crashworks

3
@Crashworks И именно поэтому Linux написан и скомпилирован как непереносимый C. (то есть надмножество C, которое требует определенного компилятора с определенными параметрами, такими как -fno-strict-aliasing)
user253751

3
@usr Я ожидаю, что он определен, если printfне возвращается, но если printfон собирается вернуться, то неопределенное поведение может вызвать проблемы до printfвызова. Следовательно, путешествие во времени. printf("Hello\n");а затем следующая строка компилируется какundoPrintf(); launchNuclearMissiles();
user253751

31

Во-первых, позвольте мне исправить заголовок этого вопроса:

Неопределенное поведение (конкретно) не из области исполнения.

Неопределенное поведение влияет на все шаги: компиляцию, связывание, загрузку и выполнение.

Несколько примеров, подтверждающих это, помните, что ни один раздел не является исчерпывающим:

  • компилятор может предполагать, что части кода, содержащие неопределенное поведение, никогда не выполняются, и, таким образом, предполагать, что пути выполнения, которые могут привести к ним, являются мертвым кодом. Посмотрите, что каждый программист на C должен знать о поведении undefined, автор - не кто иной, как Крис Латтнер.
  • компоновщик может предположить, что при наличии нескольких определений слабого символа (распознаваемого по имени) все определения идентичны благодаря Правилу одного определения
  • загрузчик (если вы используете динамические библиотеки) может принять то же самое, выбирая таким образом первый найденный символ; обычно (ab) используется для перехвата вызовов с использованием LD_PRELOADуловок в Unix
  • выполнение может завершиться неудачно (SIGSEV), если вы используете висячие указатели

Вот что так пугает в Undefined Behavior: предсказать заранее, какое именно поведение будет происходить, практически невозможно, и этот прогноз необходимо пересматривать при каждом обновлении цепочки инструментов, базовой ОС, ...


Я рекомендую посмотреть это видео Майкла Спенсера (разработчик LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .


3
Вот что меня беспокоит. В моем реальном коде это сложно, но у меня может быть случай, когда он всегда будет переполняться. И меня это особо не волнует, но я беспокоюсь, что это также повлияет на "правильный" код. Очевидно, мне нужно это исправить, но для исправления требуется понимание :)
jcoder 07

8
@jcoder: Здесь есть один важный выход. Компилятору не разрешается угадывать входные данные. Пока существует хотя бы один вход, для которого не выполняется неопределенное поведение, компилятор должен гарантировать, что этот конкретный вход по-прежнему дает правильный результат. Все страшные разговоры об опасных оптимизациях относятся только к неизбежному UB. Фактически, если бы вы использовали argcв качестве счетчика циклов, регистр argc=1не выдает UB, и компилятор был бы вынужден обработать это.
MSalters

@jcoder: В данном случае это не мертвый код. Однако компилятор может быть достаточно умен, чтобы сделать вывод, что iнельзя увеличивать более чем Nраз и, следовательно, его значение ограничено.
Матье М.

4
@jcoder: Если f(good);что-то делает X и f(bad);вызывает неопределенное поведение, тогда программа, которая просто вызывает f(good);, гарантированно выполнит X, но f(good); f(bad);не гарантированно сделает X.

4
@Hurkyl более интересно, если ваш код есть if(foo) f(good); else f(bad);, умный компилятор отбросит сравнение и создаст и безусловный foo(good).
Джон Дворжак,

28

Агрессивно оптимизирующий компилятор C или C ++, ориентированный на 16 бит, intбудет знать, что поведение при добавлении 1000000000к intтипу не определено .

Допускается любой стандарт , чтобы сделать все , что он хочет , который может включать в себя удаление всей программы, в результате чего int main(){}.

А как насчет большего ints? Я не знаю компилятора, который бы это делал (и я ни в коем случае не являюсь экспертом в проектировании компиляторов C и C ++), но я полагаю, что когда-нибудь компилятор, ориентированный на 32-битный intили выше, обнаружит, что цикл бесконечно ( iне меняется) и поэтому в aконечном итоге переполнится. Итак, еще раз, он может оптимизировать вывод до int main(){}. Я пытаюсь здесь подчеркнуть, что по мере того, как оптимизация компилятора становится все более агрессивной, все больше и больше неопределенных поведенческих конструкций проявляют себя неожиданным образом.

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


3
Разрешено ли стандартом делать все, что угодно, даже до проявления неопределенного поведения? Где это сказано?
jimifiki 07

4
почему 16 бит? Я предполагаю, что OP ищет 32-битное подписанное переполнение.
4386427 07

8
@jimifiki В стандартном. C ++ 14 (N4140) 1.3.24 «udnefined behavior = поведение, к которому данный международный стандарт не предъявляет требований». Плюс длинное пояснение. Но дело в том, что неопределенным является не поведение «оператора», а поведение программы. Это означает, что пока UB запускается правилом в стандарте (или отсутствием правила), стандарт перестает применяться для программы в целом. Так что любая часть программы может вести себя как хочет.
Энгью больше не гордится SO

5
Первое утверждение неверно. Если intэто 16 бит, добавление будет происходить в long(потому что у литерального операнда есть тип long), где он четко определен, а затем будет преобразован с помощью преобразования, определенного реализацией, обратно в int.
R .. GitHub НЕ ПОМОГАЕТ ICE

2
@usr поведение printfопределяется стандартом, чтобы всегда возвращать
MM

11

Технически, согласно стандарту C ++, если программа содержит неопределенное поведение, поведение всей программы, даже во время компиляции (до того, как программа даже будет выполнена), не определено.

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

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


Что, если часть UB находится в if(false) {}области действия? Отравляет ли это всю программу из-за того, что компилятор предполагает, что все ветви содержат ~ четко определенные части логики, и, таким образом, работает с ошибочными предположениями?
mlvljr

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

Полезно знать, спасибо :)
mlvljr

9

Чтобы понять, почему неопределенное поведение может «путешествовать во времени», как правильно выразился @TartanLlama , давайте взглянем на правило «как если бы»:

1.9 Выполнение программы

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

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

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

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

Неопределенное поведение - это отсутствие соответствия между вводом и выводом. Программа может иметь неопределенное поведение для одного ввода, но определенное поведение для другого. Тогда сопоставление между вводом и выводом просто неполное; есть вход, для которого не существует сопоставления с выходом.
Программа в вопросе имеет неопределенное поведение для любого ввода, поэтому отображение пусто.


6

Предполагая, что intэто 32-битная версия, неопределенное поведение происходит на третьей итерации. Таким образом, если, например, цикл был доступен только условно или мог быть условно завершен до третьей итерации, не было бы неопределенного поведения, если только третья итерация не была фактически достигнута. Однако в случае неопределенного поведения весь вывод программы не определен, включая вывод, который находится «в прошлом» относительно вызова неопределенного поведения. Например, в вашем случае это означает, что нет гарантии увидеть 3 сообщения «Hello» в выводе.


6

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

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

Есть пример, который я люблю публиковать, хотя признаю, что потерял источник, поэтому мне приходится перефразировать. Это было из конкретной версии MySQL. В MySQL у них был кольцевой буфер, который был заполнен данными, предоставленными пользователем. Они, конечно, хотели убедиться, что данные не переполняют буфер, поэтому у них была проверка:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

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

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Похоже, вы позаботились об ошибке переполнения буфера? Однако была отправлена ​​ошибка, в которой говорилось, что этот буфер переполнен в определенной версии Debian! Тщательное расследование показало, что эта версия Debian была первой, в которой использовалась передовая версия gcc. В этой версии gcc компилятор обнаружил, что currentPtr + numberOfNewChars никогда не может быть меньшим указателем, чем currentPtr, потому что переполнение для указателей является неопределенным поведением! Этого было достаточно для gcc, чтобы оптимизировать всю проверку, и внезапно вы оказались не защищены от переполнения буфера, даже если написали код для проверки!

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


Я не считаю особенно удивительными компиляторы, которые иногда ведут себя так, как будто знаковая арифметика выполняется с типами, диапазон которых выходит за пределы "int", особенно с учетом того, что даже при простой генерации кода на x86 бывают случаи, когда это более эффективно, чем усечение промежуточного полученные результаты. Что еще более удивительно, так это то, что переполнение влияет на другие вычисления, что может произойти в gcc, даже если код сохраняет произведение двух значений uint16_t в uint32_t - операция, у которой не должно быть веских причин для неожиданных действий в несанкционированной сборке.
supercat 07

Конечно, правильная проверка была бы if(numberOfNewChars > endOfBufferPtr - currentPtr)при условии, что numberOfNewChars никогда не может быть отрицательным, а currentPtr всегда указывает на то место в буфере, где вам даже не нужна нелепая «циклическая» проверка. (Я не думаю, что предоставленный вами код имеет хоть какую-то надежду на работу в кольцевом буфере - вы пропустили все необходимое для этого в перефразировке, поэтому я также игнорирую этот случай)
Random832

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

Это моя самая большая проблема с неопределенным поведением. Иногда это делает невозможным написать правильный код, и когда компилятор его обнаруживает, по умолчанию не сообщает вам, что это вызвало неопределенное поведение. В этом случае пользователь просто хочет выполнить арифметику - указатель или нет - и вся его тяжелая работа по написанию безопасного кода была отменена. По крайней мере, должен быть способ аннотировать часть кода, чтобы сказать - никаких модных оптимизаций здесь. C / C ++ используется во многих критических областях, чтобы позволить этой опасной ситуации сохраниться в пользу оптимизации
Джон МакГрат

4

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

for (int i=0; i<n; i++)
  foo[i] = i*scale;

компилятор может преобразовать это в:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

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

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

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

Хотя большинство таких оптимизаций не вызовут никаких проблем в случаях, когда два коротких беззнаковых типа умножаются для получения значения, которое находится между INT_MAX + 1 и UINT_MAX, gcc имеет некоторые случаи, когда такое умножение внутри цикла может вызвать ранний выход из цикла. . Я не заметил такого поведения, проистекающего из инструкций сравнения в сгенерированном коде, но оно наблюдается в тех случаях, когда компилятор использует переполнение, чтобы сделать вывод, что цикл может выполняться не более 4 раз; по умолчанию он не генерирует предупреждения в тех случаях, когда некоторые входные данные вызывают UB, а другие нет, даже если его выводы приводят к игнорированию верхней границы цикла.


4

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

С незапамятных времен программисты всегда пытались спасти остатки определенности из неопределенной ситуации. У них есть код, который они действительно хотят использовать, но который оказывается неопределенным, поэтому они пытаются возразить: «Я знаю, что он не определен, но, конечно, он, в худшем случае, сделает то или это; он никогда не сделает этого. . " И иногда эти аргументы более или менее верны, но часто они ошибочны. И по мере того, как компиляторы становятся все умнее и умнее (или, как некоторые могут сказать, изворотливее и хитрее), границы вопроса продолжают меняться.

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


и все же вот в чем дело ... компиляторы могут использовать неопределенное поведение для оптимизации, но ОНИ ВООБЩЕ НЕ ГОВОРИТ ВАМ. Итак, если у нас есть этот замечательный инструмент, с которым вы должны избегать X любой ценой, почему компилятор не может выдать вам предупреждение, чтобы вы могли его исправить?
Jason S

1

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

Однако, конечно, это само по себе не определено, потому что оптимизация не определена. :)


1
Нет причин рассматривать оптимизацию при определении того, является ли поведение неопределенным.
Кейт Томпсон,

2
Тот факт, что программа ведет себя так, как можно было бы предположить, не означает, что неопределенное поведение «исчезает». Поведение пока не определено, и вы просто полагаетесь на удачу. Сам факт того, что поведение программы может измениться в зависимости от параметров компилятора, является убедительным признаком того, что поведение не определено.
Джордан Мело,

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

@KeithThompson Конечно, но ОП специально спросил об оптимизации и ее влиянии на неопределенное поведение, которое он увидит на своей платформе. Это конкретное поведение может исчезнуть в зависимости от оптимизации. Однако, как я сказал в своем ответе, неопределенность - нет.
Грэм

0

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

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

Мы можем видеть это из Отчета о дефектах 109, который в своей сути спрашивает:

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

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Итак, главный вопрос заключается в следующем: должен ли приведенный выше код быть «успешно переведен» (что бы это ни значило)? (См. Сноску к подпункту 5.1.1.3.)

и ответ был:

Стандарт C использует термин «неопределенное значение», а не «неопределенное значение». Использование объекта с неопределенным значением приводит к неопределенному поведению. В сноске к подпункту 5.1.1.3 указывается, что реализация может производить любое количество диагностик, пока действующая программа все еще правильно переведена. Если выражение, вычисление которого привело бы к неопределенному поведению, появляется в контексте, где требуется постоянное выражение, содержащаяся программа не строго соответствует. Более того, если каждое возможное выполнение данной программы приведет к неопределенному поведению, данная программа не является строго соответствующей. Соответствующая реализация не должна отказывать в переводе строго соответствующей программы просто потому, что некоторое возможное выполнение этой программы приведет к неопределенному поведению. Поскольку foo может никогда не вызываться, приведенный пример должен быть успешно переведен соответствующей реализацией.

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

У нас есть [intro.abstrac] p5, в котором говорится:

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


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

@supercat Я считаю, что это то, что я говорю нам, по крайней мере, для C.
Шафик Ягмур

Я думаю, то же самое применимо и к цитируемому тексту на C ++, поскольку фраза «Любое такое выполнение» относится к способам выполнения программы с конкретным заданным вводом. Если конкретный ввод не может привести к выполнению функции, я не вижу в цитируемом тексте ничего, что говорило бы о том, что что-либо в такой функции приведет к UB.
supercat

-2

Главный ответ - неправильное (но распространенное) заблуждение:

Неопределенное поведение - это свойство времени выполнения *. Это НЕ МОЖЕТ "путешествовать во времени"!

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

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

Фактически, это согласуется с цитатой в верхнем ответе (выделено мной):

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

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

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

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

* Примечание: это не противоречит UB, возникающему во время компиляции . Если компилятор действительно может доказать , что UB код будет всегда выполняться для всех входов, то UB может распространяться на время компиляции. Однако для этого необходимо знать, что весь предыдущий код в конечном итоге вернется , что является серьезным требованием. Опять же, см. Ниже пример / объяснение.


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

printf("foo");
getchar();
*(char*)1 = 1;

Однако также обратите внимание, что нет гарантии, что fooон останется на экране после появления UB или что набранный вами символ больше не будет во входном буфере; обе эти операции можно «отменить», что имеет эффект, аналогичный UB «путешествия во времени».

Если бы getchar()строки не было, то оптимизация строк была бы законной тогда и только тогда , когда это было бы неотличимо от вывода, fooа затем «отмены».

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

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

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

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


1
Вы полностью неверно истолковываете стандарт. В нем говорится, что поведение при выполнении программы не определено. Период. Этот ответ на 100% неверен. Стандарт очень ясен - запуск программы с вводом, который производит UB в любой точке наивного потока выполнения, не определен.
Дэвид Шварц

@DavidSchwartz: Если вы доведете свою интерпретацию до логических выводов, вы должны понять, что она не имеет логического смысла. Вход - это не то, что полностью определяется при запуске программы. Вход в программу (даже его простое присутствие ) в любой данной строке может зависеть от всех побочных эффектов программы до этой строки. Следовательно, программа не может избежать побочных эффектов, которые появляются перед линией UB, потому что это требует взаимодействия с ее средой и, следовательно, влияет на то, будет ли достигнута линия UB или нет.
user541686

3
Это не имеет значения. В самом деле. Опять же, вам просто не хватает воображения. Например, если компилятор может сказать, что никакой совместимый код не может отличить разницу, он может переместить код, который является UB, так, чтобы часть, выполняемая UB, выполнялась до выходных данных, которые вы наивно ожидаете «предшествовать».
Дэвид Шварц

2
@Mehrdad: Возможно, лучший способ сказать что-то - сказать, что UB не может путешествовать во времени назад после последней точки, где что-то могло произойти в реальном мире, что определило бы поведение. Если бы реализация могла определить, исследуя входные буферы, что никакие из следующих 1000 вызовов getchar () не могут быть заблокированы, и она также могла бы определить, что UB произойдет после 1000-го вызова, то не потребовалось бы выполнять какие-либо из следующих действий: звонки. Если, однако, реализация должна указать, что выполнение не будет передавать getchar (), пока все предыдущие выходные данные не будут ...
supercat

2
... был доставлен на 300-бодовый терминал, и что любой элемент control-C, который происходит до этого, заставит getchar () поднять сигнал, даже если в буфере перед ним были другие символы, тогда такая реализация не могла переместить любой UB за последний вывод, предшествующий getchar (). Сложно знать, в каком случае компилятор должен передать программисту какие-либо поведенческие гарантии, которые реализация библиотеки может предложить помимо тех, которые предусмотрены стандартом.
supercat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.