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

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

int x = x;

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

Это на самом деле неопределенное поведение?

В соответствии с п. 4.1 "Преобразование Lvalue-в-значение", это не определенное поведение для выполнения преобразования Lvalue-в-значение для неинициализированного значения. Делает правую x пройти это преобразование? Если да, то будет ли пример иметь неопределенное поведение?

4 ответа

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


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


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

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

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

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

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

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

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

Другое доказательство найдено в 4/8, как отметил Johannes Schaub - litb в комментариях к связанным вопросам и ответам:

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

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


ГИПОТЕЗА:

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

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

Обычные стандартные преобразования lvalue-to-rvalue (4.1), array-to-pointer (4.2) и function-to-pointer (4.3) не нужны и, следовательно, подавляются, когда такие прямые привязки к lvalue выполняются.

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

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

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

Согласно этому предположению в вашем примере потребуется преобразование lvalue в rvalue, что приведет к неопределенному поведению.


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

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

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

Более того, как правильно отмечает Jesse Good в своем ответе, ключевой параграф стандарта C++ - 8.5/16:

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

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

  1. Будут ли выполнены преобразования категорий, если это необходимо?
  2. Они нужны?

Что касается второго вопроса, как обсуждалось в оригинальной части ответа, стандарт C++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 = "Да", и мы должны иметь неопределенное поведение.


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

В этом отчете о дефектах (любезно предоставлено Джесси Гудом) предлагается изменение, направленное на предоставление неопределенного поведения в этом случае:

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

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

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

- Если T является (возможно, cv-квалифицированным) 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]), и объект, на который ссылается glvalue, содержит неопределенное значение (5.3.4 [expr.new], 8.5 [dcl.init], 12.6.2 [class.base.init]), и этот объект не имеет автоматической продолжительности хранения, или glvalue был операндом унарного оператора & или он был связан со ссылкой, результатом является неопределенное значение. [Сноска: значение может отличаться каждый раз, когда преобразование lvalue в rvalue применяется к объекту. Неподписанный объект типа char с неопределенным значением, выделенным для регистра, может быть перехвачен. - сноска]

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

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

Неявная последовательность преобразования выражения 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;, результат последовательности преобразования является предварительным значением, потому что int это не ссылочный тип. Так что, если мы предоставляем glvalue, нам явно нужна конверсия. 3.10p2 далее разъясняет, что не должно быть никаких сомнений

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

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

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

Что вы можете найти в https://github.com/llvm-mirror/clang/blob/master/test/Sema/uninit-variables.c#L48 для этого случая явно.

Другие вопросы по тегам