Включает ли инициализация преобразование lvalue-to-rvalue? Является ли `int x = x;` UB?

Стандарт С++ содержит полуизвестный пример "удивительного" поиска имен в разделе 3.3.2 "Точка декларации":

int x = x;

Инициализирует x с самим собой, который (будучи примитивным типом) неинициализирован и, следовательно, имеет неопределенное значение (если он является автоматической переменной).

Действительно ли это поведение undefined?

В соответствии с 4.1 "преобразованием Lvalue-to-rvalue" поведение undefined выполняет преобразование lvalue-to-rvalue по неинициализированному значению. Соответствует ли правое x такое преобразование? Если это так, действительно ли пример имел поведение undefined?

Ответ 1

ОБНОВЛЕНИЕ:. После обсуждения в комментариях я добавил еще несколько доказательств в конце этого ответа.


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


В контексте этого Q & A выяснилось, что в стандарте С++ 11 не удается формально указать, что категории значений ожидаются каждой конструкцией языка. В следующем я буду в основном сосредоточен на встроенных операциях, хотя вопрос об инициализаторах. В конце концов, я в конечном итоге продолжу выводы, которые я нарисовал для случая операторов, к случаю инициализаторов.

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

Например, в примечании в пункте 3.10/1 говорится:

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

Раздел 5.17 о операторах присваивания, с другой стороны, не упоминает об этом. Тем не менее, возможность выполнения преобразования lvalue-rvalue упоминается снова в примечании (пункт 5.17/1):

Следовательно, вызов функции не должен вмешиваться между преобразованием lvalue-to-r и побочным эффектом, связанным с любым единственным оператором присваивания

Конечно, если бы никакой оценки не ожидалось, эта нота была бы бессмысленной.

Еще одно доказательство найдено в 4/8, как указано Йоханнесом Шаубом в комментариях к связанным Q & A:

Существуют некоторые контексты, в которых определенные преобразования подавляются. Например, преобразование lvalue-to-rvalue не выполняется в операнде унарного и оператора. Конкретные исключения приводятся в описаниях этих операторов и контекстов.

Это означает, что преобразование lvalue-rvalue выполняется для всех операндов встроенных операторов, за исключением случаев, когда указано иное. Это будет означать, в свою очередь, что rvalues ​​ожидается как операнды встроенных операторов, если не указано иное.


ГИПОТЕЗА:

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

Следы, подтверждающие это убеждение, можно найти даже в параграфе 8.5.2/5 об инициализации ссылок (для которых значение выражения инициализатора lvalue не требуется):

Стандартные конверсии обычного lvalue-to-rvalue (4.1), array-to-pointer (4.2) и функции-to-pointer (4.3) не требуются и поэтому подавляются, когда такие прямые привязки к lvalues ​​выполняются.

Слово "обычный", по-видимому, подразумевает, что при инициализации объектов, которые не относятся к ссылочному типу, предполагается применение преобразования lvalue-rvalue.

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

Везде, где значение требуется для конструкции языка, ожидается ожидаемое значение, если не указано иное.

В этом предположении в вашем примере потребуется преобразование lvalue-rvalue, и это приведет к Undefined Поведение.


ДОПОЛНИТЕЛЬНЫЕ ДОКАЗАТЕЛЬСТВА:

Чтобы предоставить дополнительные доказательства для поддержки этой гипотезы, допустим, что это неправильно, так что для инициализации копирования не требуется преобразование lvalue-rvalue и рассмотрим следующий код (благодаря jogojapan за вклад):

int y;
int x = y; // No UB
short t;
int u = t; // UB! (Do not like this non-uniformity, but could accept it)
int z;
z = x; // No UB (x is not uninitialized)
z = y; // UB! (Assuming assignment operators expect a prvalue, see above)
       // This would be very counterintuitive, since x == y

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

Более того, как Джесси Добрый правильно указывает в своем ответе, ключевой Параграф Стандарта С++ - 8.5/16:

- В противном случае начальным значением инициализируемого объекта является (возможно, преобразованное) значение выражения инициализатора. стандарт конверсии (раздел 4), при необходимости, чтобы преобразовать выражение инициализации для cv-неквалифицированной версии тип; не учитываются определенные пользователем преобразования. Если преобразование не может быть выполнено, инициализация плохо сформирована. [ Заметка: Выражение типа "cv1 T" может инициализировать объект типа "cv2 T" независимо от cv-квалификаторов cv1 и cv2.

Однако, в то время как Джесси главным образом фокусируется на битке "если необходимо", я также хотел бы подчеркнуть слово " тип". В приведенном выше параграфе упоминается, что стандартные преобразования будут использоваться "при необходимости" для преобразования в тип адресата, но ничего не говорят о конверсиях категорий:

  • При необходимости будут выполняться преобразования категорий?
  • Нужны ли они?

Что касается второго вопроса, как обсуждалось в первоначальной части ответа, то в стандарте С++ 11 в настоящее время не указывается, нужны ли преобразования категорий или нет, потому что нигде не упоминается, ожидает ли инициализация копий prvalue как инициализатор. Таким образом, однозначный ответ невозможно дать. Тем не менее, я считаю, что предоставил достаточно доказательств, чтобы предположить, что это намеченная спецификация, так что ответ будет "Да" .

Что касается первого вопроса, мне кажется разумным, что ответ "Да" . Если бы это было "Нет", очевидно, правильные программы были бы плохо сформированы:

int y = 0;
int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct)

Подводя итог (A1 = "Ответ на вопрос 1", A2 = "Ответ на вопрос 2" ):

          | A2 = Yes   | A2 = No |
 ---------|------------|---------|
 A1 = Yes |     UB     |  No UB  | 
 A1 = No  | ill-formed |  No UB  |
 ---------------------------------

Если A2 "Нет", A1 не имеет значения: нет UB, но появляются странные ситуации первого примера (например, z = y, дающие UB, но не z = x, хотя x == y). Если А2 "Да" , с другой стороны, А1 становится критическим; тем не менее, достаточно доказательств, подтверждающих, что это будет "Да" .

Следовательно, мой тезис: A1 = "Да" и A2 = "Да" , и мы должны иметь Undefined Поведение.


ДАЛЬНЕЙШИЕ ДОКАЗАТЕЛЬСТВА:

Этот отчет (любезно предоставлен Jesse Good) предлагает изменения, направленные на предоставление Undefined Поведения в этом случае:

[...] Кроме того, в 4.1 параграфе 4.1 [conv.lval] говорится, что применение преобразования lvalue-to-rval в "объект [, который] неинициализирован" приводит к поведению Undefined; это нужно перефразировать в терминах объекта с неопределенным значением.

В частности, в предлагаемой формулировке пункта 4.1 говорится:

Когда преобразование lvalue-rvalue происходит в неоцененном операнде или его подвыражении (раздел 5 [expr]), значение, содержащееся в ссылочном объекте, не получает доступа. Во всех остальных случаях результат преобразования определяется в соответствии со следующими правилами:

- Если T (возможно, cv-qualid) std:: nullptr_t, результатом является константа нулевого указателя (4.10 [conv.ptr]).

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

- В противном случае, если объект, к которому относится ссылка glvalue, содержит недопустимое значение указателя (3.7.4.2 [basic.stc.dynamic.deallocation], 3.7.4.3 [basic.stc.dynamic.safety]), поведение от реализации.

- В противном случае, если T является (возможно, cv-квалифицированным) типом неподписанного символа (3.9.1 [basic.fundamental]), а объект, к которому относится ссылка gl, содержит неопределенное значение (5.3.4 [expr.new ], 8.5 [dcl.init], 12.6.2 [class.base.init]), и этот объект не имеет автоматической продолжительности хранения, или glvalue является операндом унарного и оператора или он привязан к ссылке, результатом является неопределенное значение. [Сноска: значение может быть различным каждый раз, когда преобразование lvalue-rvalue применяется к объекту. Объект без знака char с неопределенным значением, назначенным регистру, может ловить ловушку. -end footnote]

- В противном случае, если объект, к которому относится glvalue, содержит неопределенное значение, поведение undefined.

- В противном случае, если glvalue имеет (возможно, cv-qualit) тип std:: nullptr_t, результат prvalue является константой нулевого указателя (4.10 [conv.ptr]). В противном случае значение, содержащееся в объекте, обозначенном glvalue, является результатом prvalue.

Ответ 2

Неявная последовательность преобразований выражения e для типа T определяется как эквивалентная следующей декларации с использованием T в результате преобразования (категория по модулю), которая будет определена в зависимости от T), 4p3 и 4p6

T t = e;

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

В разделе 4 преобразование выражения в тип всегда дает выражения с определенным свойством. Например, преобразование 0 в int* дает значение нулевого указателя, а не только одно произвольное значение указателя. Категория значения также является определенным свойством выражения, и его результат определяется следующим образом:

Результатом является lvalue, если T является ссылочным типом lvalue или ссылкой rvalue к типу функции (8.3.2), xvalue, если T является ссылкой rvalue к типу объекта и в противном случае значением prvalue.

Следовательно, мы знаем, что в int t = e; результат последовательности преобразования является prvalue, потому что int является не ссылочным типом. Поэтому, если мы предоставляем glvalue, мы явно нуждаемся в конверсии. 3.10p2 далее поясняет, что не оставлять сомнений

Всякий раз, когда glvalue появляется в контексте, где ожидается значение prvalue, glvalue преобразуется в prvalue; см. 4.1, 4.2 и 4.3.

Ответ 3

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

Ответ 4

Поведение не undefined. Переменная неинициализируется и остается с каким-либо случайным значением, не инициализируемым значениями. Один пример из теста clan'g:

int test7b(int y) {
  int x = x; // expected-note{{variable 'x' is declared here}}
  if (y)
    x = 1;
  // Warn with "may be uninitialized" here (not "is sometimes uninitialized"),
  // since the self-initialization is intended to suppress a -Wuninitialized
  // warning.
  return x; // expected-warning{{variable 'x' may be uninitialized when used here}}
}

Что вы можете найти в clang/test/Sema/uninit-variables.c для этого случая явно.