三向比较和constexpr函数模板:哪个编译器正确?

问题描述

考虑:

#include <compare>

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

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

GCC和MSVC接受T的声明。 Clang拒绝它,并显示以下错误消息:

<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

如果模板头(template<class=void>)被删除,或者f在声明T之前被显式或隐式实例化,则Clang接受它。例如,Clang接受:

#include <compare>

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

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

live demo

哪个编译器正确,为什么?

解决方法

C N4861正确。

[temp.inst]/5

除非功能模板专长是已声明的专长,否则当在需要存在函数定义的上下文中引用该专长或如果定义的存在影响程序的语义时,将隐式实例化功能模板专长。 / p>

[temp.inst]/8

如果通过表达式([expr.const])进行常量评估需要变量或函数,则认为存在变量或函数定义会影响程序的语义。

[expr.const]/15

需要常量或常量的函数

  • 由表达式([basic.def.odr])命名的constexpr函数,该表达式可能是常量,或者
  • 变量[...]。

[expr.const]/15

如果表达式或转换为:可能是常量,如果是

  • 一个明显为常量值的表达式
  • 可能评估的表达式([basic.def.odr])
  • 括号初始列表的直接子表达式
  • 出现在模板实体中的形式为&cast-expression的表达式,或者
  • 上述之一的子表达式,不是嵌套的未求值操作数的子表达式。

[expr.const]/5

表达式 E 是一个核心常量表达式,除非遵循抽象机器的规则对[em> E 进行评估([intro.execution ]),将评估以下其中一项:

  • [...]
  • 未定义的constexpr函数的调用;

[dcl.init.list]/7

缩小转换是隐式转换

  • [...]
  • 从整数类型或非范围枚举类型到不能表示原始类型所有值的整数类型,除非源是一个常量表达式,其整数提升后的值将适合目标类型

[expr.spaceship]/4

如果两个操作数都具有算术类型,或者一个操作数具有整数类型,而另一个操作数具有非作用域枚举类型,则通常的算术转换将应用于这些操作数。 然后:

  • 如果需要缩小转换,而不是从整数类型转换为浮点类型,则程序格式错误。

[expr.arith.conv]

[T]通常的算术转换[...]定义如下:

  • [...]
  • 否则,应在两个操作数上执行整数提升([conv.prom])。 然后,以下规则将应用于提升后的操作数:
    • [...]
    • 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则应将具有符号整数类型的操作数转换为具有无符号整数类型的操作数的类型。

由于x <=> f()中的decltype(x <=> f())不满足被“潜在不变评估”的标准,因此f不需要“不变评估”。因此,f<>定义的存在不被认为会影响程序的语义。因此,该表达式不会实例化f<>的定义。

因此,在原始示例中,f()是对未定义constexpr函数的调用,该函数不是常量表达式。

按照通常的算术转换,在x <=> f()中,f()(类型为int)将转换为unsigned int。如果f()不是常数表达式,则此转换是缩小转换,这会使程序格式错误。

如果f不是函数模板,或者其定义已被实例化,则f() 一个常量表达式,并且因为{{1} }符合f(),从unsigned intf()的转换不是缩小的转换,因此程序格式正确。