Если у меня есть:
unsigned int x;
x -= x;
ясно, что после этого выражения x
должно быть ноль, но куда бы я ни посмотрел, они говорят, что поведение этого кода не определено, а не просто значение x
(до вычитания).
Два вопроса:
Действительно ли поведение этого кода не определено?
(Например, может произойти сбой кода [или того хуже] в совместимой системе?)Если да, то почему C говорит, что поведение не определено, когда совершенно ясно, что здесь
x
должно быть ноль?т.е. какое преимущество дает отсутствие здесь определения поведения?
Ясно, что компилятор может просто использовать любое мусорное значение, которое он посчитает «удобным» внутри переменной, и он будет работать так, как задумано ... что не так в таком подходе?
c
undefined-behavior
initialization
пользователь541686
источник
источник
x -= x
. Вопрос в том, почему доступ к неинициализированным значениям вообще осуществляется через UB.Ответы:
Да, это поведение не определено, но по другим причинам, о которых большинство людей не знает.
Во-первых, использование унифицированного значения само по себе не является неопределенным поведением, но значение просто неопределенно. Доступ к этому - UB, если значение является представлением ловушки для типа. Беззнаковые типы редко имеют представления прерывания, так что с этой стороны вы будете в относительной безопасности.
Неопределенным поведение делает дополнительное свойство вашей переменной, а именно то, что она «могла быть объявлена с помощью
register
», то есть ее адрес никогда не используется. Такие переменные обрабатываются особым образом, поскольку существуют архитектуры с реальными регистрами ЦП, которые имеют своего рода дополнительное состояние, которое является «неинициализированным» и не соответствует значению в области типов.Редактировать: соответствующая фраза стандарта 6.3.2.1p2:
И чтобы сделать его более четким, следующий код является законным при любых обстоятельствах:
unsigned char a, b; memcpy(&a, &b, 1); a -= a;
a
иb
, поэтому их значение просто неопределенно.unsigned char
никогда не было представлений прерывания, что неопределенное значение просто не указано, любое значениеunsigned char
могло произойти.a
должно быть сохранено значение0
.Edit2:
a
иb
имеют неопределенные значения:источник
unsigned
s наверняка есть представления ловушек. Можете ли вы указать на часть стандарта, в которой это сказано? В §6.2.6.2 / 1 я вижу следующее: «Для беззнаковых целочисленных типов, отличных от unsigned char , биты представления объекта должны быть разделены на две группы: биты значения и биты заполнения (последних может не быть). ... это должно быть известно как представление значения. Значения любых битов заполнения не определены. ⁴⁴⁾ "с комментарием:" ⁴⁴⁾ Некоторые комбинации битов заполнения могут генерировать представления прерывания ".unsigned char
, но этот ответ используетunsigned char
. Однако обратите внимание: строго соответствующая программа может вычислятьsizeof(unsigned) * CHAR_BIT
и определять, основываясь на томUINT_MAX
, что конкретные реализации не могут иметь представления ловушекunsigned
. После того, как эта программа сделала это определение, она может продолжить делать именно то, что делает этот ответunsigned char
.memcpy
отвлекает, то есть не стал бы ваш пример по-прежнему применяться, если бы его заменили на*&a = *&b;
.unsigned char
и, следовательно,memcpy
помогает,*&
менее ясен. Я сообщу, когда все уляжется.Стандарт C дает компиляторам большую свободу действий для оптимизации. Последствия этих оптимизаций могут быть удивительными, если вы предположите наивную модель программ, в которой неинициализированная память настроена на некоторый случайный битовый шаблон и все операции выполняются в том порядке, в котором они написаны.
Примечание: следующие примеры действительны только потому,
x
что его адрес никогда не использовался, поэтому он «похож на регистр». Они также были бы допустимы, если бы типx
имел представления ловушек; это редко случается с беззнаковыми типами (это требует «траты» по крайней мере одного бита памяти и должно быть задокументировано) и невозможно дляunsigned char
. Еслиx
бы был тип со знаком, то реализация могла бы определить битовый шаблон, который не является числом между - (2 n-1 -1) и 2 n-1 -1, в качестве представления ловушки. См . Ответ Йенса Густедта .Компиляторы пытаются присвоить регистры переменным, потому что регистры быстрее памяти. Поскольку программа может использовать больше переменных, чем регистров процессора, компиляторы выполняют распределение регистров, что приводит к тому, что разные переменные используют один и тот же регистр в разное время. Рассмотрим фрагмент программы
unsigned x, y, z; /* 0 */ y = 0; /* 1 */ z = 4; /* 2 */ x = - x; /* 3 */ y = y + z; /* 4 */ x = y + 1; /* 5 */
Когда строка 3 оценивается,
x
она еще не инициализирована, поэтому (по причинам компилятора) строка 3 должна быть какой-то случайностью, которая не может произойти из-за других условий, которые компилятор не был достаточно умен, чтобы выяснить. Посколькуz
не используется после строки 4 иx
не используется перед строкой 5, для обеих переменных можно использовать один и тот же регистр. Итак, эта небольшая программа скомпилирована для следующих операций с регистрами:r1 = 0; r0 = 4; r0 = - r0; r1 += r0; r0 = r1;
Конечное значение
x
- это конечное значениеr0
, а конечное значениеy
- это конечное значениеr1
. Это значения x = -3 и y = -4, а не 5 и 4, как если быx
они были правильно инициализированы.Для более подробного примера рассмотрим следующий фрагмент кода:
unsigned i, x; for (i = 0; i < 10; i++) { x = (condition() ? some_value() : -x); }
Предположим, что компилятор обнаруживает, что у
condition
него нет побочного эффекта. Посколькуcondition
не изменяетx
, компилятор знает, что при первом прогоне цикла невозможно получить доступ,x
поскольку он еще не инициализирован. Следовательно, первое выполнение тела цикла эквивалентноx = some_value()
, нет необходимости проверять условие. Компилятор может скомпилировать этот код, как если бы вы написалиunsigned i, x; i = 0; /* if some_value() uses i */ x = some_value(); for (i = 1; i < 10; i++) { x = (condition() ? some_value() : -x); }
Это можно смоделировать внутри компилятора, если учесть, что любое значение, зависящее от,
x
имеет любое удобное значение, еслиx
оно не инициализировано. Поскольку поведение, когда неинициализированная переменная не определена, а не переменная, имеющая просто неопределенное значение, компилятору не нужно отслеживать какие-либо особые математические отношения между любыми удобными значениями. Таким образом, компилятор может проанализировать приведенный выше код следующим образом:x
не инициализируется к моменту-x
оценки.-x
имеет неопределенное поведение, поэтому его значение не имеет значения.condition ? value : value
condition; value
Столкнувшись с кодом в вашем вопросе, этот же компилятор анализирует, что при
x = - x
оценке значение-x
- это все, что удобно. Таким образом, задание можно оптимизировать.Я не искал примера компилятора, который ведет себя так, как описано выше, но именно такой вид оптимизации пытаются делать хорошие компиляторы. Я не удивлюсь, если с ним столкнусь. Вот менее правдоподобный пример компилятора, с которым происходит сбой вашей программы. (Это может быть не так уж неправдоподобно, если вы компилируете свою программу в каком-либо расширенном режиме отладки.)
Этот гипотетический компилятор отображает каждую переменную на другой странице памяти и настраивает атрибуты страницы таким образом, чтобы чтение из неинициализированной переменной приводило к ловушке процессора, вызывающей отладчик. Любое присвоение переменной сначала гарантирует, что ее страница памяти отображается нормально. Этот компилятор не пытается выполнять какую-либо расширенную оптимизацию - он находится в режиме отладки, предназначенном для легкого обнаружения ошибок, таких как неинициализированные переменные. Когда
x = - x
выполняется оценка, правая сторона вызывает прерывание и запускается отладчик.источник
x
имеет неинициализированное значение, но поведение при доступе будет быть определенным, если x не имеет поведения, подобного регистру.x
, то все операции над ним могут быть опущены, независимо от того, было ли его значение определено или нет. Если следующий код, напримерif (volatile1) x=volatile2; ... x = (x+volatile3) & 255;
, будет в равной степени удовлетворен любым значением 0-255, котороеx
может содержаться в случае, когдаvolatile1
он дал ноль, я бы подумал, что реализация, которая позволит программисту опустить ненужную запись,x
должна рассматриваться как более качественная, чем та, которая будет вести себя ...Да, программа может вылететь. Например, могут быть представления прерываний (определенные битовые шаблоны, которые не могут быть обработаны), которые могут вызвать прерывание ЦП, а необработанное может привести к сбою программы.
(Это объяснение применимо только к платформам, на которых
unsigned int
могут быть представления ловушек, что редко в реальных системах; см. Комментарии для деталей и ссылок на альтернативные и, возможно, более общие причины, которые приводят к текущей формулировке стандарта.)источник
(Этот ответ относится к C 1999 г. Для C 2011 см. Ответ Йенса Густедта.)
Стандарт C не говорит, что использование значения объекта с автоматической продолжительностью хранения, которое не инициализировано, является неопределенным поведением. Стандарт C 1999 говорит в 6.7.8 10: «Если объект с автоматической продолжительностью хранения не инициализирован явно, его значение не определено». (В этом параграфе описывается, как инициализируются статические объекты, поэтому единственными неинициализированными объектами, которые нас беспокоят, являются автоматические объекты.)
3.17.2 определяет «неопределенное значение» как «либо неопределенное значение, либо представление прерывания». 3.17.3 определяет «неуказанное значение» как «действительное значение соответствующего типа, если настоящий международный стандарт не налагает требований к выбору значения в любом случае».
Таким образом, если неинициализированное значение
unsigned int x
имеет неопределенное значение, оноx -= x
должно давать ноль. Остается вопрос, может ли это быть ловушкой. Доступ к значению прерывания вызывает неопределенное поведение согласно 6.2.6.1 5.Некоторые типы объектов могут иметь представления прерывания, например, сигнальные NaN чисел с плавающей запятой. Но беззнаковые целые числа особенные. Согласно 6.2.6.2, каждый из N битов значений беззнакового int представляет степень 2, а каждая комбинация битов значений представляет одно из значений от 0 до 2 N -1. Таким образом, целые числа без знака могут иметь представление прерывания только из-за некоторых значений в их битах заполнения (например, бит четности).
Если на вашей целевой платформе unsigned int не имеет битов заполнения, тогда неинициализированный unsigned int не может иметь представление ловушки, и использование его значения не может вызвать неопределенное поведение.
источник
x
есть представление ловушки, тоx -= x
может ловушка, верно? Тем не менее, +1 для указания беззнаковых целых чисел без дополнительных битов должен иметь определенное поведение - это явно противоположно другим ответам и (согласно цитате), похоже, это то, что подразумевает стандарт.x
имеет представление ловушки, тоx -= x
может быть ловушка. Даже простоеx
использование в качестве значения может вызвать ловушку. (Это безопасно использоватьx
в качестве lvalue; на запись в объект не повлияет содержащееся в нем представление ловушки.)Да, это не определено. Код может дать сбой. C говорит, что поведение не определено, потому что нет особых причин делать исключение из общего правила. Преимущество такое же преимущество, как и во всех других случаях неопределенного поведения - компилятору не нужно выводить специальный код, чтобы это работало.
Как вы думаете, почему этого не происходит? Это именно тот подход. Компилятор не обязан заставлять его работать, но и не заставляет его отказывать.
источник
x
его можно объявить какregister
, то есть его адрес никогда не используется. Я не знаю, знали ли вы об этом (если вы эффективно это скрывали), но правильный ответ должен упомянуть об этом.Для любой переменной любого типа, которая не инициализирована или по другим причинам содержит неопределенное значение, для кода, считывающего это значение, применяется следующее:
В противном случае, если нет представлений прерываний, переменная принимает неопределенное значение. Нет гарантии, что это неопределенное значение будет постоянным при каждом чтении переменной. Однако гарантируется, что это не представление ловушки, и поэтому гарантируется, что он не вызовет неопределенное поведение [3].
Затем значение можно безопасно использовать, не вызывая сбоя программы, хотя такой код не переносится в системы с представлениями прерываний.
[1]: C11 6.3.2.1:
[2]: C11 6.2.6.1:
[3] C11:
источник
stdint.h
всегда следует использовать вместо собственных типов C. Потому чтоstdint.h
принудительное дополнение до 2 и отсутствие битов заполнения. Другими словами,stdint.h
типы не могут быть полны дерьма.Хотя во многих ответах основное внимание уделяется процессорам, которые блокируют доступ к неинициализированному регистру, странное поведение может возникнуть даже на платформах, которые не имеют таких ловушек, при использовании компиляторов, которые не прилагают особых усилий для использования UB. Рассмотрим код:
volatile uint32_t a,b; uin16_t moo(uint32_t x, uint16_t y, uint32_t z) { uint16_t temp; if (a) temp = y; else if (b) temp = z; return temp; }
компилятор для платформы, подобной ARM, где все инструкции, кроме загрузки и сохранения, работают с 32-битными регистрами, может разумно обрабатывать код способом, эквивалентным:
volatile uint32_t a,b; // Note: y is known to be 0..65535 // x, y, and z are received in 32-bit registers r0, r1, r2 uin32_t moo(uint32_t x, uint32_t y, uint32_t z) { // Since x is never used past this point, and since the return value // will need to be in r0, a compiler could map temp to r0 uint32_t temp; if (a) temp = y; else if (b) temp = z & 0xFFFF; return temp; }
Если любое из непостоянных чтений дает ненулевое значение, r0 будет загружено со значением в диапазоне 0 ... 65535. В противном случае он выдаст то, что содержалось при вызове функции (то есть значение, переданное в x), которое может не быть значением в диапазоне 0..65535. В Стандарте отсутствует какая-либо терминология для описания поведения значения, тип которого - uint16_t, но значение которого выходит за пределы диапазона 0..65535, за исключением того, что сказано, что любое действие, которое может вызвать такое поведение, вызывает UB.
источник
uint16_t
, эта переменная иногда может читаться как 123, а иногда как 6553623). Если результат будет проигнорирован ...register
, то у нее могут быть дополнительные биты, которые делают поведение потенциально неопределенным. Это именно то, что вы говорите, верно?