(Почему) использует неинициализированную переменную неопределенное поведение?
Если у меня есть:
unsigned int x;
x -= x;
понятно что x
должно быть равно нулю после этого выражения, но везде, где я смотрю, говорят, что поведение этого кода не определено, а не просто значение x
(пока до вычитания).
Два вопроса:
Действительно ли поведение этого кода не определено?
(Например, может ли сбой кода [или хуже] на совместимой системе?)Если так, то почему C говорит, что поведение не определено, когда совершенно ясно, что
x
здесь должен быть ноль?то есть, какое преимущество дает отсутствие определения поведения здесь?
Ясно, что компилятор может просто использовать любое значение мусора, которое он считает "удобным" внутри переменной, и он будет работать как задумано... что не так с этим подходом?
7 ответов
Да, это поведение не определено, но по другим причинам, чем большинство людей знают.
Во-первых, использование унифицированного значения само по себе не является неопределенным поведением, но значение просто неопределенное. Доступ к этому тогда - UB, если значение оказывается представлением ловушки для типа. Типы без знака редко имеют представление ловушек, поэтому вы были бы относительно безопасны на этой стороне.
То, что делает поведение неопределенным, является дополнительным свойством вашей переменной, а именно то, что оно "могло быть объявлено с помощью register
"то есть его адрес никогда не берется. Такие переменные обрабатываются специально, потому что есть архитектуры, которые имеют реальные регистры ЦП, которые имеют своего рода дополнительное состояние, которое" неинициализировано "и не соответствует значению в домене типа.
Изменить: соответствующая фраза стандарта 6.3.2.1p2:
Если lvalue обозначает объект с автоматической продолжительностью хранения, который мог быть объявлен с помощью класса хранения регистра (никогда не было взято его адрес), и этот объект не был инициализирован (не объявлен с помощью инициализатора и никакое присвоение ему не было выполнено до использования), поведение не определено.
И чтобы было понятнее, следующий код допустим при любых обстоятельствах:
unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
- Здесь адреса
a
а такжеb
взяты, поэтому их значение просто неопределенно. - поскольку
unsigned char
никогда не имеет ловушек представления, что неопределенное значение просто не определено, любое значениеunsigned char
могло случиться. - В конце
a
должен держать значение0
,
Edit2: a
а также b
имеют неопределенные значения:
3.19.3 неопределенное значение
действительное значение соответствующего типа, если данный международный стандарт не предъявляет требований к выбору значения в любом случае
Стандарт 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
оценивается, правая часть вызывает ловушку, и отладчик запускается.
Да, программа может аварийно завершить работу. Например, могут быть представления прерываний (определенные битовые комбинации, которые не могут быть обработаны), которые могут вызвать прерывание ЦП, что может привести к аварийному завершению программы.
(6.2.6.1 в поздней версии C11 гласит) Некоторые представления объектов не должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, которое не имеет символьного типа, поведение не определено. Если такое представление создается побочным эффектом, который изменяет весь или любую часть объекта выражением lvalue, которое не имеет символьного типа, поведение не определено.50) Такое представление называется представлением ловушки.
(Это объяснение применяется только на платформах, где unsigned int
может иметь представление ловушек, что редко встречается в реальных системах; см. комментарии для получения подробной информации и ссылок на альтернативные и, возможно, более распространенные причины, которые приводят к текущей редакции стандарта.)
(Этот ответ относится к C 1999. Информацию о C 2011 см. В ответе Jens Gustedt.)
Стандарт 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 битов значения беззнакового целого представляет степень 2, и каждая комбинация битов значения представляет одно из значений от 0 до 2N-1. Таким образом, целые числа без знака могут иметь представление ловушек только из-за некоторых значений в их битах заполнения (таких как бит четности).
Если на вашей целевой платформе неподписанный тип int не имеет битов заполнения, то неинициализированный тип unsigned int не может иметь представление прерывания, а использование его значения не может привести к неопределенному поведению.
Для любой переменной любого типа, которая не инициализирована или по другим причинам имеет неопределенное значение, для кода, читающего это значение, применяется следующее:
- В случае, если переменная имеет автоматическую продолжительность хранения и не имеет взятого адреса, код всегда вызывает неопределенное поведение [1].
- В противном случае, если система поддерживает представления ловушек для данного типа переменной, код всегда вызывает неопределенное поведение [2].
В противном случае, если нет ловушек, переменная принимает неопределенное значение. Нет никаких гарантий, что это неуказанное значение будет постоянным при каждом чтении переменной. Однако гарантируется, что он не является представлением ловушек, и поэтому гарантируется, что он не вызовет неопределенное поведение [3].
Затем это значение можно безопасно использовать, не вызывая сбой программы, хотя такой код не переносится в системы с представлениями прерываний.
[1]: C11 6.3.2.1:
Если lvalue обозначает объект с автоматической продолжительностью хранения, который мог быть объявлен с помощью класса хранения регистра (никогда не было взято его адрес), и этот объект не был инициализирован (не объявлен с помощью инициализатора и никакое присвоение ему не было выполнено до использования), поведение не определено.
[2]: C11 6.2.6.1:
Определенные представления объекта не должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, которое не имеет символьного типа, поведение не определено. Если такое представление создается побочным эффектом, который изменяет весь или любую часть объекта выражением lvalue, которое не имеет символьного типа, поведение не определено.50) Такое представление называется представлением ловушки.
[3] C11:
3.19.2
неопределенное значение
либо неопределенное значение, либо представление ловушки3.19.3
неопределенное значение
действительное значение соответствующего типа, если данный международный стандарт не предъявляет требований к выбору значения в любом случае
ПРИМЕЧАНИЕ. Неуказанное значение не может быть представлением прерывания.3.19.4
представление ловушки
представление объекта, которое не обязательно должно представлять значение типа объекта
Да, это не определено. Код может привести к сбою. C говорит, что поведение не определено, потому что нет особой причины делать исключение из общего правила. Преимущество такое же, как и во всех других случаях неопределенного поведения - компилятору не нужно выводить специальный код для этой работы.
Ясно, что компилятор может просто использовать любое значение мусора, которое он считает "удобным" внутри переменной, и он будет работать как задумано... что не так с этим подходом?
Почему вы думаете, что этого не происходит? Это именно тот подход, который принят. Компилятор не требуется, чтобы заставить его работать, но это не обязательно, чтобы это терпело неудачу.
Хотя многие ответы сосредоточены на процессорах, которые улавливают доступ к неинициализированным регистрам, причудливое поведение может возникать даже на платформах, у которых нет таких ловушек, с использованием компиляторов, которые не прилагают особых усилий для использования 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.