Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Three-way comparison and constexpr function template: which compiler is right?

Consider:

#include <compare>

template<class=void>
constexpr int f() { return 1; }

unsigned int x;
using T = decltype(x <=> f());

GCC and MSVC accept the declaration of T. Clang rejects it, with the following error message:

<source>:7:26: error: argument to 'operator<=>' cannot be narrowed from type 'int' to 'unsigned int'
using T = decltype(x <=> f());
                        ^
1 error generated.

(live demo)

If the template-head (template<class=void>) is removed, or if f is explicitly or implicitly instantiated before the declaration of T, then Clang accepts it. For example, Clang accepts:

#include <compare>

template<class=void>
constexpr int f() { return 1; }

unsigned x;
auto _ = x <=> f();
using T = decltype(x <=> f());

(live demo)

Which compiler is correct, and why?

like image 353
cpplearner Avatar asked Nov 02 '20 10:11

cpplearner


People also ask

Are Constexpr functions evaluated at compile time?

A constexpr function that is eligible to be evaluated at compile-time will only be evaluated at compile-time if the return value is used where a constant expression is required. Otherwise, compile-time evaluation is not guaranteed.

What is Constexpr used for?

constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time. A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations.

Does Constexpr improve performance?

In Conclusion. constexpr is an effective tool for ensuring compile-time evaluation of function calls, objects and variables. Compile-time evaluation of expressions often leads to more efficient code and enables the compiler to store the result in the system's ROM.


1 Answers

Clang is correct per N4861.

[temp.inst]/5:

Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program.

[temp.inst]/8:

The existence of a definition of a variable or function is considered to affect the semantics of the program if the variable or function is needed for constant evaluation by an expression ([expr.const])

[expr.const]/15:

A function or variable is needed for constant evaluation if it is:

  • a constexpr function that is named by an expression ([basic.def.odr]) that is potentially constant evaluated, or
  • a variable [...].

[expr.const]/15:

An expression or conversion is potentially constant evaluated if it is:

  • a manifestly constant-evaluated expression,
  • a potentially-evaluated expression ([basic.def.odr]),
  • an immediate subexpression of a braced-init-list,
  • an expression of the form & cast-expression that occurs within a templated entity, or
  • a subexpression of one of the above that is not a subexpression of a nested unevaluated operand.

[expr.const]/5:

An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

  • [...]
  • an invocation of an undefined constexpr function;

[dcl.init.list]/7:

A narrowing conversion is an implicit conversion

  • [...]
  • from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type

[expr.spaceship]/4:

If both operands have arithmetic types, or one operand has integral type and the other operand has unscoped enumeration type, the usual arithmetic conversions are applied to the operands. Then:

  • If a narrowing conversion is required, other than from an integral type to a floating-point type, the program is ill-formed.

[expr.arith.conv]:

[T]he usual arithmetic conversions [...] are defined as follows:

  • [...]
  • Otherwise, the integral promotions ([conv.prom]) shall be performed on both operands. Then the following rules shall be applied to the promoted operands:
    • [...]
    • Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.

Since x <=> f() in decltype(x <=> f()) does not satisfy the criteria of being "potentially constant evaluated", f is not "needed for constant evaluation". Therefore, the existence of the definition of f<> is not considered to affect the semantics of the program. Therefore, this expression does not instantiate the definition of f<>.

Therefore, in the original example, f() is a call to undefined constexpr function, which is not a constant expression.

Per the usual arithmetic conversions, in x <=> f(), f() (of type int) is converted to unsigned int. When f() is not a constant expression, this conversion is a narrowing conversion, which renders the program ill-formed.

If f is not a function template, or if its definition has been instantiated, then f() is a constant expression, and because the result of f() fits into unsigned int, the conversion from f() to unsigned int is not a narrowing conversion, and thus the program is well-formed.

like image 199
cpplearner Avatar answered Sep 29 '22 06:09

cpplearner