Является ли поведение a или a-a undefined, если a не инициализировано?

Рассмотрим эту программу:

#include <stdio.h>

int main(void)
{
    unsigned int a;
    printf("%u %u\n", a^a, a-a);
    return 0;
}

Это поведение undefined?

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

Ответ 1

В C11:

  • Он явно undefined в соответствии с 6.3.2.1/2, если a никогда не имеет своего адреса (цитируется ниже)
  • Это может быть ловушечное представление (которое вызывает UB при доступе). 6.2.6.1/5:

Определенные представления объектов не обязательно должны представлять значение типа объекта.

Unsigned ints может иметь ловушечные представления (например, если у него есть 15 прецизионных бит и 1 бит четности, доступ к a может вызвать ошибку четности).

6.2.4/6 говорит, что начальное значение является неопределенным, а определение под 3.19.2 является либо неопределенным значением, либо ловушечным представлением.

Далее: в C11 6.3.2.1/2, как указано Паскалем Куоком:

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

Это не имеет исключения для типов символов, поэтому это предложение заменяет предыдущее обсуждение; доступ к x происходит немедленно undefined, даже если представления ловушки не существуют. Этот раздел был добавлен в C11 для поддержки процессоров Itanium, которые фактически имеют состояние ловушки для регистров.


Системы без ловушек: Но что, если мы введем &x;, чтобы это возражение 6.3.2.1/2 больше не применялось, и мы находимся на системе, которая, как известно, не имеет никакой ловушки представления? Тогда значение является неопределенным значением. Определение неопределенного значения в 3.19.3 немного расплывчато, однако оно поясняется DR 451, которое заключает:

  • Неинициализированное значение в описанных условиях может изменить его значение.
  • Любая операция, выполняемая с неопределенными значениями, будет иметь неопределенное значение в результате.
  • Функции библиотеки будут демонстрировать поведение undefined при использовании на неопределенных значениях.
  • Эти ответы подходят для всех типов, у которых нет ловушек.

В этом разрешении int a; &a; int b = a - a; приводит к тому, что b имеет неопределенное значение.

Обратите внимание, что если неопределенное значение не передается библиотечной функции, мы все еще находимся в области неопределенного поведения (не undefined). Результаты могут быть странными, например. if ( j != j ) foo(); может вызвать foo, но демоны должны оставаться в полости носа.

Ответ 2

Да, это поведение undefined.

Во-первых, любая неинициализированная переменная может иметь "разбитое" (или "ловушечное" ) представление. Даже одна попытка доступа к этому представлению вызывает поведение undefined. Более того, даже объекты типов нелокализации (например, unsigned char) могут по-прежнему приобретать специальные зависящие от платформы состояния (например, NaT - Not-A-Thing - Itanium), которые могут появляться как проявление их "неопределенного значения".

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

Ответ 3

Если объект имеет автоматическую продолжительность хранения и его адрес не берется, попытка прочитать его даст Undefined Поведение. Принимая адрес такого объекта и используя указатели типа "unsigned char" для считывания их байтов, гарантируется стандартом, чтобы получить значение типа "unsigned char", но не все компиляторы придерживаются Стандарт в этом отношении. ARM GCC 5.1, например, при предоставлении:

  #include <stdint.h>
  #include <string.h>
  struct q { uint16_t x,y; };
  volatile uint16_t zz;
  int32_t foo(uint32_t x, uint32_t y)
  {
    struct q temp1,temp2;
    temp1.x = 3;
    if (y & 1)
      temp1.y = zz;
    memmove(&temp2,&temp1,sizeof temp1);
    return temp2.y;
  }

будет генерировать код, который будет возвращать x, если y равно нулю, даже если x находится вне диапазона 0-65535. Стандарт дает понять, что беззнаковые символы, считываемые из неопределенного значения, гарантируют, что они дают значение в диапазоне unsigned char, а поведение memmove определяется как эквивалентное последовательности символов чтения и записи. Таким образом, temp2 должен иметь значение, которое может быть записано в него посредством последовательности записей символов, но gcc решает заменить memmove назначением и игнорировать тот факт, что код принял адрес temp1 и temp2. ​​

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