Составное назначение в функции 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 всегда переводится в число с плавающей запятой.