Последовательность с ++ 17 в назначении: все еще не реализовано в GCC?

Я попробовал следующий код как наивную попытку реализовать замену байтов R и B в слове ABGR

#include <stdio.h>
#include <stdint.h>

uint32_t ABGR_to_ARGB(uint32_t abgr)
{
  return ((abgr ^= (abgr >> 16) & 0xFF) ^= (abgr & 0xFF) << 16) ^= (abgr >> 16) & 0xFF;
}

int main() 
{
    uint32_t tmp = 0x11223344;
    printf("%x %x\n", tmp, ABGR_to_ARGB(tmp));
}

К моему удивлению, этот код "работал" в GCC в режиме C ++ 17 - байты поменялись местами

http://coliru.stacked-crooked.com/a/43d0fc47f5539746

Но поменять местами байты не предполагается! В C ++ 17 четко указано, что RHS присваивания должен быть [полностью] упорядочен перед LHS, что также применимо к составному присваиванию. Это означает, что в приведенном выше выражении предполагается, что каждая правая часть каждого ^= использует исходное значение abgr. Следовательно, конечный результат в abgr должен просто иметь B байтов, соединенных с помощью XOR на R байтов. Это то, что, кажется, производит Clang (что забавно, с предупреждением о последовательности)

http://coliru.stacked-crooked.com/a/eb9bdc8ced1b5f13

Быстрый взгляд на сборку GCC

https://godbolt.org/g/1hsW5a

показывает, что он, кажется, упорядочивает его в обратном порядке: LHS перед RHS. Это ошибка? Или это какое-то осознанное решение со стороны GCC? Или я что-то недопонимаю?


person AnT    schedule 25.07.2018    source источник
comment
Я не вижу очевидной ошибки в вашем коде, поэтому я считаю, что это ошибка gcc, поскольку их статус довольно ясен: P0145R3 поддерживается в версии 7 и примечания тоже согласны. По крайней мере, они исправили более очевидных случаев. Clang и MSVS , похоже, поступают правильно   -  person Shafik Yaghmour    schedule 25.07.2018
comment
Попробуйте g++ -Wall.   -  person n. 1.8e9-where's-my-share m.    schedule 25.07.2018
comment
@ n.m. он дает неупорядоченное предупреждение, которое я считаю неправильным, поскольку новая формулировка обеспечивает последовательность. Даже если вы удалите это, используя отдельные значения, он все равно будет делать то же самое.   -  person Shafik Yaghmour    schedule 25.07.2018
comment
@ShafikYaghmour Предупреждение просто намекает вам о какой-то проблеме. Более простое воспроизведение - int x = 1; (x *= (x+3)) *= (x+7);. Оно вызывает одинаковые предупреждения от обоих компиляторов и дает разные результаты. - н.м. 5 минут назад   -  person n. 1.8e9-where's-my-share m.    schedule 25.07.2018
comment
@ n.m. это уродливый код, я бы попросил рефакторинга в обзоре кода. У меня достаточно сомнений, что я не буду отвечать, я бы предпочел очевидный случай, свободный от потенциальных UB. В этом случае кажется, что gcc поступает правильно.   -  person Shafik Yaghmour    schedule 25.07.2018
comment
@ShafikYaghmour ну, очевидно, gcc думает, что это UB, но так ли это на самом деле?   -  person n. 1.8e9-where's-my-share m.    schedule 25.07.2018


Ответы (2)


Точно такое же поведение демонстрирует int a = 1; (a += a) += a;, для которого GCC затем вычисляет a == 4 и clang a == 3.

Основная неоднозначность возникает из этой части стандарта (из рабочего проекта N4762):

[expr.ass]: 7.6.18 Операторы присваивания и составные операторы присваивания

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

Параграф 7. Поведение выражения вида E1 op = E2 эквивалентно E1 = E1 op E2, за исключением того, что E1 оценивается только один раз. В + = и - = E1 должен иметь либо арифметический тип, либо быть указателем на возможно полностью определенный тип объекта с квалификацией cv. Во всех остальных случаях E1 должен иметь арифметический тип.

Кажется, что GCC использует это правило для внутреннего преобразования из (a += a) += a в (a = a + a) += a в a = (a = a + a) + a (поскольку a = a + a должен оцениваться только один раз) - и для этого выражения правила последовательности применяются правильно.

Однако Clang, похоже, выполняет этот последний шаг преобразования по-другому: auto temp = a + a; temp = temp + a; a = temp;

Однако оба компилятора предупреждают об этом (из исходного кода):

  • GCC: warning: operation on 'abgr' may be undefined [-Wsequence-point]

  • лязг: warning: unsequenced modification and access to 'abgr' [-Wunsequenced]

Таким образом, авторы компилятора знают об этой двусмысленности и решили установить приоритеты по-другому (GCC: Параграф 7> Параграф 1; clang: Параграф 1> Параграф 7).

Похоже, это дефект стандарта.

person hoffmale    schedule 25.07.2018

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

uint32_t ABGR_to_ARGB(uint32_t abgr) {
    constexpr uint32_t mask = 0xff00ff00;
    uint32_t grab = abgr >> 16 | abgr << 16;
    return (abgr & mask) | (grab & ~mask);
}

Он также обеспечивает гораздо лучшую сборку, чем исходная версия. На x86 он использует одну инструкцию rol для трех побитовых операторов для создания grab:

ABGR_to_ARGB(unsigned int):
        mov     eax, edi
        and     edi, -16711936
        rol     eax, 16
        and     eax, 16711935
        or      eax, edi
        ret
person Maxim Egorushkin    schedule 25.07.2018
comment
Вопрос не в том, как лучше переставить байты одним словом. Исходный код никоим образом не предназначен для решения исходной проблемы. Это была не более чем попытка использовать [ранее недействительный] трюк a ^= b ^= a ^= b и посмотреть, что будет. - person AnT; 25.07.2018