Связанные составные назначения с секвенированием С ++ 17 все еще не определены?

Изначально я привел более сложный пример, этот был предложен @n. 'местоимения' м. в теперь удаленном ответе. Но вопрос стал слишком длинным, смотрите историю редактирования, если вам интересно.

Имеет ли следующая программа четко определенное поведение на C ++ 17?

int main()
{
    int a=5;
    (a += 1) += a;
    return a;
}

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

  1. Правая сторона a оценивается как 5.
  2. Побочных эффектов правой стороны нет.
  3. Левая часть оценивается как ссылка на a, a += 1 точно определена.
  4. Выполняется левосторонний побочный эффект, в результате чего получается a==6.
  5. Присваивание вычисляется, добавляя 5 к текущему значению a, что делает его 11.

Соответствующие разделы стандарта:

[intro.execution] / 8:

Выражение X считается упорядоченным перед выражением Y, если каждое вычисление значения и каждый побочный эффект, связанный с выражением X, упорядочен перед каждым вычислением значения и каждым побочным эффектом, связанным с выражением Y.

[expr.ass] / 1 (выделено мной):

Оператор присваивания (=) и составные операторы присваивания группируются справа налево. Все требуют изменяемого lvalue в качестве левого операнда; их результат - lvalue, относящийся к левому операнду. Результатом во всех случаях является битовое поле, если левый операнд является битовым полем. Во всех случаях присваивание выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания. Правый операнд упорядочивается перед левым операндом. Что касается вызова функции с неопределенной последовательностью, операция составного присваивания является однократной оценкой.

Формулировка изначально взята из принятой статьи P0145R3 < / а>.

Я чувствую, что во втором разделе есть некоторая двусмысленность и даже противоречие.

Правый операнд ставится перед левым операндом.

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

Во всех случаях присваивание выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания.

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

  1. Правая часть a оценивается как 5.
  2. Левая сторона оценивается как ссылка a, a += 1 точно определена.
  3. Присваивание вычисляется, добавляя 5 к текущему значению a, что делает его 10.
  4. Выполняется левый побочный эффект, делая a==11 или даже 6, если старые значения использовались даже для побочного эффекта.

Но этот порядок явно нарушает определение упорядочено до, поскольку побочные эффекты левого операнда произошли после вычисления значения правого операнда. Таким образом, левый операнд не был упорядочен после правого операнда, что нарушает указанное выше предложение. Нет, я пошутил. Это допустимое поведение, правда? Т.е. присвоение может чередовать оценку справа-слева. Или это можно сделать после обеих полных оценок.

Выполнение кода gcc выводит 12, clang 11. Кроме того, gcc предупреждает о

<source>: In function 'int main()':

<source>:4:8: warning: operation on 'a' may be undefined [-Wsequence-point]
    4 |     (a += 1) += a;
      |     ~~~^~~~~

Я ужасно читаю сборку, может кто-нибудь сможет хоть как то переписать gcc до 12? (a += 1), a+=a работает, но это кажется неправильным.

Что ж, если подумать, правая сторона также оценивает ссылку на a, а не только на значение 5. Так что Gcc все еще может быть прав, в этом случае clang может быть неправильным.


person Quimby    schedule 04.10.2020    source источник
comment
Чтение сборки не помогает отладить вычисления времени компиляции, поскольку компилятор выводит их результаты.   -  person Öö Tiib    schedule 04.10.2020
comment
Зачем настаивать на использовании *= и += в одной строке? Просто разбейте его на две строчки. Даже если он был упорядочен правильно, он все равно будет нечитаемым для людей (или увеличит когнитивную нагрузку, если вы хотите более мягкое утверждение)   -  person Jeffrey    schedule 04.10.2020
comment
@ Джеффри Я прямо заявил, что меня не волнует читабельность такого кода, я хочу знать, почему он не работает. Как написать выражение сгиба в две строки?   -  person Quimby    schedule 04.10.2020
comment
Правило №20 на странице cppreference не имеет значения, поскольку когда вычисляются константы 2 и Args, не имеет значения. Другим важным правилом является правило № 8: побочный эффект (модификация левого аргумента) встроенного оператора присваивания и всех встроенных операторов составного присваивания упорядочивается после вычисления значения (но не побочных эффектов) обоих. левый и правый аргументы, и упорядочивается перед вычислением значения выражения присваивания (то есть перед возвратом ссылки на измененный объект).   -  person aschepler    schedule 04.10.2020
comment
@aschepler Да, №20 не нужен в первом случае, но я считаю, что он уместен в правом-левом. Я не заметил этого правила. Итак, вы говорите, что res*=2 возвращает ссылку, поскольку вычисление значения должно произойти до ref+=..., но остается неупорядоченным с учетом, когда res*=2 действительно выполняется. Таким образом, приращение может произойти до умножения? Кажется, противоречит № 20. Тем не менее, вы можете дать ответ?   -  person Quimby    schedule 04.10.2020
comment
Да верно, вам нужен # 20 для порядка операндов |=. Я не написал ответа, потому что до сих пор не уверен, в чем здесь проблема.   -  person aschepler    schedule 04.10.2020
comment
@aschepler Я отредактировал вопрос с продолжением, но не могу сказать, что это сделало меня мудрее :( Возможно (a += 1) += a; просто не определено, но я не понимаю, как это сделать.   -  person Quimby    schedule 04.10.2020
comment
Я думаю, это в любом случае должен быть отдельный вопрос.   -  person cigien    schedule 04.10.2020
comment
@cigien Я думал об этом, но это оставит этот вопрос без ответа, и заголовок уже правильный, на него еще нет ответов. Только первый представленный пример был излишне сложным.   -  person Quimby    schedule 04.10.2020
comment
Хорошо, но теперь вопрос довольно сложный. Если вы уверены, что проблема может быть уменьшена до (a += 1) += a;, удалите все остальное. В противном случае задайте новый вопрос об этом и укажите ссылку на этот вопрос в качестве мотивации, если хотите. (Обратите внимание, что вопрос уже требовал 2 вещей, объяснения и исправления. Это может быть хорошим способом разделить это).   -  person cigien    schedule 04.10.2020
comment
@cigien Я сильно его урезал :). Да, если это не определено, мой пример тоже. В противном случае я могу попросить продолжить более сложный пример.   -  person Quimby    schedule 04.10.2020
comment
Да, теперь это намного яснее. Да, если это не UB, то неплохо было бы опубликовать исходный вопрос отдельно. Еще немного покрутил метки.   -  person cigien    schedule 04.10.2020
comment
@cigien Спасибо за помощь:]   -  person Quimby    schedule 04.10.2020
comment
Следует отметить, что поведение для типа пользователя согласовано как в clang, так и в gcc и придерживается последовательности до: простой случай и свернуть выражение. Это не закрывает пробел для примитивных типов, но может быть решением для достижения согласованных выражений свертки.   -  person Amir Kirsh    schedule 04.10.2020
comment
Правая сторона также оценивает ссылку на a - я думаю, что преобразование lvalue-to-rvalue требуется для [basic.lval] / 6 для операнда следует рассматривать как вычисление значения этого операнда. Возможно, и определение идентификатора glvalue, и доступ для чтения преобразования lvalue-to-rvalue - это два вычисления значений одного и того же выражения?   -  person aschepler    schedule 04.10.2020
comment
Предупреждение gcc может быть ошибкой. Вот еще один. Странно Clang в том, что оценка в static_assert, по-видимому, выполняется в другом порядке, и результаты отличаются от оценки за пределами static_assert. Это приемлемо, если поведение не определено, однако я считаю, что если clang считает операции неупорядоченными, он должен предупредить об этом, как это происходит с -std=c++14 (где они не упорядочены).   -  person n. 1.8e9-where's-my-share m.    schedule 04.10.2020
comment
@ n.'pronouns'm. UB и unsequenced очень разные. Первое должно быть диагностировано в постоянном выражении. См. static_assert().   -  person Deduplicator    schedule 04.10.2020
comment
@Deduplicator Неупорядоченный доступ и модификация одной и той же переменной is UB.   -  person n. 1.8e9-where's-my-share m.    schedule 05.10.2020


Ответы (1)


Чтобы лучше понять, что происходит на самом деле, давайте попробуем имитировать то же самое с нашим собственным шрифтом и добавим распечатки:

class Number {
    int num = 0;
public:
    Number(int n): num(n) {}
    Number operator+=(int i) {
        std::cout << "+=(int) for *this = " << num
                  << " and int = " << i << std::endl;
        num += i;
        return *this;
    }
    Number& operator+=(Number n) {
        std::cout << "+=(Number) for *this = " << num
                  << " and Number = " << n << std::endl;
        num += n.num;
        return *this;
    }
    operator int() const {
        return num;
    }
};

Затем, когда мы бежим:

Number a {5};
(a += 1) += a;
std::cout << "result: " << a << std::endl;

Мы получаем разные результаты с gcc и clang (и без всякого предупреждения!).

gcc:

+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 6
result: 12

лязг:

+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 5
result: 11

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

Кажется, что хотя gcc сохраняет правую сторону в качестве ссылки и превращает ее в значение при вызове + =, clang, с другой стороны, превращает правую сторону в значение в первую очередь.

Следующим шагом будет добавление конструктора копирования в наш класс Number, чтобы точно следовать, когда ссылка превращается в значение. Выполнение этого приводит к вызову конструктора копирования в качестве первой операции, как clang и gcc, и результат будет одинаковым для обоих: 11.

Похоже, что gcc задерживает ссылку на преобразование значения (как во встроенном назначении, так и в случае определенного пользователем типа без определяемого пользователем конструктора копирования). Является ли это последовательным с C ++ 17 определенная последовательность? Мне это кажется ошибкой gcc, по крайней мере, для встроенного присваивания, как в вопросе, поскольку кажется, что преобразование из ссылки в значение является частью вычисления значения , который должен быть упорядочен перед назначением.


Что касается странного поведения clang, о котором сообщалось в предыдущей версии исходного сообщения - возврат разных результатов в assert и при печати:

constexpr int foo() {
    int res = 0;
    (res = 5) |= (res *= 2);
    return res;
}

int main() {
    std::cout << foo() << std::endl; // prints 5
    assert(foo() == 5); // fails in clang 11.0 - constexpr foo() is 10
                        // fixed in clang 11.x - correct value is 5
}

Это связано с ошибкой в ​​clang. Ошибка утверждения неверна и связана с неправильным порядком оценки этого выражения в clang во время постоянной оценки во время компиляции. Значение должно быть 5. Эта ошибка уже исправлена ​​ в магистрали clang.

person Amir Kirsh    schedule 04.10.2020
comment
Я считаю, что совершил ошибку, мой второй порядок может быть разрешен - назначение может чередовать правую и левую последовательность. В этом случае да, вычисление значения gcc кажется отрывочным, но clang может быть правильным с моим объяснением. Возможно, он не определен именно из-за чередования, если вычисление значения приводит к ссылке. - person Quimby; 04.10.2020
comment
похоже, что gcc рассматривает (a += 1) += a; и a += (a += 1) как эквиваленты из-за скобок и из-за этого откладывает преобразование ссылок. Это поведение одинаково для всех версий gcc, поскольку до C ++ 11 - person Swift - Friday Pie; 05.10.2020