Составное назначение в функции constexpr: gcc vs. clang

template<class A, class B> constexpr int f(A a, B b) {
    a /= b;
    return a;
}

constexpr int x = f(2, 2);   // a, b: int
constexpr int y = f(2., 2.); // a, b: double
constexpr int z = f(2, 2.);  // a: int, b: double //<-- BOOM!
constexpr int w = f(2., 2);  // a: double, b: int

int main() {}

Код не компилируется в clang, он производит следующую диагностику:

error: constexpr variable 'z' must be initialized by a constant expression

MSVC разбился (согласно godbolt) и gcc работает отлично. Если a/= b просто заменяется на a = a/b то все его принимают. Зачем?

Кто прав? Кажется, это связано с неявным сужением преобразования, но тогда почему a = a/b работает?

Ответ 1

Это просто ошибка clang, если мы посмотрим на составное назначение [expr.ass] p7, это эквивалентно присваиванию, где E1 оценивается только один раз:

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

Если мы посмотрим в ограничениях на требование постоянной функции выражения в [dcl.constexpr] p3, мы не имеем никаких ограничений на присваивание:

Определение функции constexpr должно удовлетворять следующим требованиям:

  • (3.1) его тип возврата должен быть буквальным;
  • (3.2) каждый из его типов параметров должен быть буквальным типом;
  • (3.3) его тело функции не должно содержать.
  • (3.3.1) определение asm,
  • (3.3.2) утверждение goto,
  • (3.3.3) метку идентификатора ([stmt.label]),
  • (3.3.4) определение переменной нелитерального типа или статической или продолжительности хранения потоков или для которой не выполняется инициализация.
    [Примечание. Функциональное тело, которое = delete или = default, не содержит ничего из вышеперечисленного. - конечная нота]

и ничего в [expr.const] не добавляет ограничений для этого конкретного случая.

Я связался с Ричардом Смитом в автономном режиме, и он согласился, что это ошибка, и сказал:

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

Зарегистрированный отчет об ошибках clang и сообщение об ошибке MSVC

Ответ 2

Я сделал патч для Clang, который должен исправить ошибку Clang.

Некоторые лягушки внутренние детали:

В clang оценка константного выражения в основном обрабатывается в lib/AST/ExprConstant.cpp. В частности, сложное присвоение целого числа обрабатывается CompoundAssignSubobjectHandler::found(APSInt &Value, QualType SubobjType). До моего патча эта функция неправильно отклоняла любые нецелые RHS:

    if (!SubobjType->isIntegerType() || !RHS.isInt()) {
      // We don't support compound assignment on integer-cast-to-pointer
      // values.
      Info.FFDiag(E);
      return false;
    }

Мой патч исправляет это, добавляя ветку для RHS.isFloat().

Обратите внимание, что подобная проблема не возникает, когда LHS - это число с плавающей запятой, а RHS - целое число, даже если CompoundAssignSubobjectHandler обрабатывает только случай с плавающей запятой <op> = float, поскольку в этом случае RHS всегда переводится в число с плавающей запятой.