(Почему) использует поведение неинициализированной переменной undefined?

Если у меня есть:

unsigned int x;
x -= x;

ясно, что x должно быть равно нулю после этого выражения, но везде, где я смотрю, они говорят, что поведение этого кода undefined, а не только значение x (до до вычитания).

Два вопроса:

  • Действительно ли поведение этого кода undefined?
    (Например, может ли произойти сбой кода [или хуже] на совместимой системе?)

  • Если это так, , почему говорит, что поведение undefined, когда совершенно ясно, что x здесь должен быть здесь?

    то есть. Какое преимущество дает не определение поведения здесь?

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

Ответ 1

Да, это поведение undefined, но по разным причинам, чем большинство людей знают.

Во-первых, использование унифицированного значения само по себе не является undefined, но значение просто неопределенно. Доступ к этому тогда является UB, если это значение является ловушкой для типа. Неподписанные типы редко имеют ловушечные представления, поэтому вы были бы относительно безопасны с этой стороны.

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

Изменить: Соответствующая фраза стандарта - 6.3.2.1p2:

Если lvalue обозначает объект с автоматическим временем хранения, который может быть объявлено с классом хранения регистров (никогда не было его адрес), и этот объект неинициализирован (не объявлен с инициализатором и никакое присвоение ему не было выполнено ранее для использования), поведение undefined.

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

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • Здесь указаны адреса a и b, поэтому их значение просто неопределенные.
  • Так как unsigned char никогда не имеет ловушных представлений что неопределенное значение просто не указано, любое значение unsigned char может произойдет.
  • В конце a должно быть указано значение 0.

Edit2: a и b имеют неуказанные значения:

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

Ответ 2

Стандарт 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 неинициализировано. Поскольку поведение, когда неинициализированная переменная имеет значение undefined, а не переменная, просто имеющая неопределенное значение, компилятору не нужно отслеживать какую-либо специальную математическую взаимосвязь между любыми удобными значениями. Таким образом, компилятор может проанализировать приведенный выше код следующим образом:

  • во время первой итерации цикла x не инициализируется временем -x.
  • -x имеет поведение undefined, поэтому его значение является любым удобным.
  • Применяется правило оптимизации condition ? value : value, поэтому этот код можно упростить до condition; value.

Когда вы сталкиваетесь с кодом в своем вопросе, этот же компилятор анализирует, что когда x = - x оценивается, значение -x является любым удобным. Таким образом, назначение можно оптимизировать.

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

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

Ответ 3

Да, программа может произойти сбой. Например, могут быть ловушечные представления (конкретные битовые шаблоны, которые невозможно обработать), которые могут вызвать прерывание ЦП, которое необработанное может привести к сбою программы.

(6.2.6.1 в конце проекта C11) Определенные представления объектов не должны представлять значение тип объекта. Если хранимое значение объекта имеет такое значение представления и считывается выражением lvalue, которое не имеет тип символа, поведение undefined. Если такое представление созданный побочным эффектом, который изменяет всю или любую часть объекта выражением lvalue, которое не имеет типа символа, поведение undefined.50). Такое представление называется ловушкой представление.

(Это объяснение применяется только на платформах, где unsigned int может иметь ловушечные представления, что редко встречается в реальных системах мира, см. комментарии для деталей и рефералов для альтернативных и, возможно, более распространенных причин, приводящих к стандартной текущей формулировке.)

Ответ 4

(Этот ответ относится к C 1999. На C 2011 см. ответ Йенса Густедта.)

В стандарте C не говорится, что использование значения времени автоматической продолжительности хранения, которое не инициализировано, - это поведение undefined. В стандарте C 1999 говорится, что в пункте 6.7.8 10 "Если объект с автоматической продолжительностью хранения не инициализирован явно, его значение неопределенно". (В этом параграфе далее определяется, как инициализируются статические объекты, поэтому единственными неинициализированными объектами, о которых нас беспокоят, являются автоматические объекты.)

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

Итак, если неинициализированный unsigned int x имеет неуказанное значение, тогда x -= x должен произвести нуль. Это оставляет вопрос о том, может ли это быть ловушечным представлением. Доступ к значению ловушки вызывает поведение undefined, в соответствии с 6.2.6.1 5.

Некоторые типы объектов могут иметь ловушечные представления, такие как сигнальные NaN с номерами с плавающей запятой. Но целые числа без знака являются особыми. В соответствии с 6.2.6.2 каждый из N битов значения беззнакового int представляет собой мощность 2, и каждая комбинация битов значений представляет одно из значений от 0 до 2 N -1. Таким образом, целые числа без знака могут иметь ловушечные представления только из-за некоторых значений в их битах заполнения (таких как бит четности).

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

Ответ 5

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

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

Почему вы думаете, что этого не происходит? Это именно тот подход. Компилятор не обязан заставлять его работать, но не требуется, чтобы он терпел неудачу.

Ответ 6

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

  • Если переменная имеет время автоматического хранения и не имеет своего адреса, код всегда вызывает undefined поведение [1].
  • В противном случае, если система поддерживает ловушки для данного типа переменной, код всегда вызывает поведение undefined [2].
  • В противном случае, если нет ловушечных представлений, переменная принимает неопределенное значение. Нет никакой гарантии, что это неопределенное значение будет согласованным каждый раз при чтении переменной. Тем не менее, гарантировано, что это не будет ловушечным представлением, и поэтому гарантировано не использовать поведение undefined [3].

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


[1]: C11 6.3.2.1:

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

[2]: C11 6.2.6.1:

Определенные представления объектов не обязательно должны представлять значение типа объекта. Если сохраненный значение объекта имеет такое представление и считывается выражением lvalue, которое делает не имеют характера, поведение undefined. Если такое представление создается побочным эффектом, который изменяет всю или любую часть объекта с помощью выражения lvalue, которое не имеет типа символа, поведение undefined.50) Такое представление называется представление ловушки.

[3] C11:

3.19.2
неопределенное значение
либо неопределенное значение, либо представление ловушки

3.19.3
неопределенное значение
действительное значение соответствующего типа, если настоящий международный стандарт не налагает требования, по которым значение выбирается в любом случае
ПРИМЕЧАНИЕ Неопределенное значение не может быть ловушечным представлением.

3.19.4
ловушечное представление
представление объекта, которое не должно представлять значение типа объекта

Ответ 7

В то время как многие ответы сосредоточены на процессорах, которые ловутся на доступ к неинициализированному регистру, причудливые поведения могут возникать даже на платформах, которые не имеют таких ловушек, используя компиляторы, которые не делают особых усилий для использования 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.