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

22

请考虑:

#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.

(实时演示)

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

#include <compare>

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

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

(演示)

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

1个回答

15

根据N4861,Clang是正确的。

[temp.inst]/5:

除非函数模板特化是一个已声明的特化,否则当该特化在需要存在函数定义的上下文中被引用,或者该定义的存在影响程序的语义时,函数模板特化会隐式实例化。

[temp.inst]/8:

如果变量或函数需要被表达式([expr.const])常量计算使用,那么定义变量或函数的存在将会影响程序的语义。

[expr.const]/15:

如果函数或变量被以下情况之一所满足,它就需要进行常量表达式求值: - 一个constexpr函数被一个可能进行常量表达式求值的表达式([basic.def.odr])所命名。 - 一个变量[...]。

[expr.const]/15:

一个表达式或转换是潜在常量求值的,如果它是:
  • 明显的常量求值表达式,
  • 可能求值的表达式([basic.def.odr]),
  • 大括号初始化列表的直接子表达式,
  • 在模板实体内出现的& cast-expression形式的表达式,或者
  • 上述任意一项的子表达式,但不是嵌套未求值操作数的子表达式。

[expr.const]/5:

表达式E是一个核心常量表达式,除非根据抽象机器([intro.execution])的规则评估E将评估以下之一:一个未定义的constexpr函数的调用;{{...}}。

[dcl.init.list]/7:

{{缩窄转换}}是一种隐式转换,从整数类型或未作用域的枚举类型到无法表示原始类型所有值的整数类型,除非源是一个常量表达式,在积分提升后其值将适合目标类型。

[expr.spaceship]/4:

如果两个操作数都具有算术类型,或者一个操作数具有整数类型,而另一个操作数具有未作用域的枚举类型,则将应用通常的算术转换到操作数。然后: 如果需要缩小转换,除了从整数类型到浮点类型的转换之外,则程序是非法的。

[expr.arith.conv]:

通常的算术转换定义如下: - 如果两个操作数中有一个是 long double,则将另一个操作数转换为 long double。 - 否则,如果其中一个操作数是 double,则将另一个操作数转换为 double。 - 否则,如果其中一个操作数是 float,则将另一个操作数转换为 float。 - 否则,如果其中一个操作数是 unsigned long long,则将另一个操作数转换为 unsigned long long。 - 否则,如果其中一个操作数是 long long,则将另一个操作数转换为 long long。 - 否则,如果其中一个操作数是 unsigned long,则将另一个操作数转换为 unsigned long。 - 否则,如果其中一个操作数是 long,则将另一个操作数转换为 long。 - 否则,如果其中一个操作数是 unsigned int,则将另一个操作数转换为 unsigned int。 - 否则,两个操作数都被提升为 int 或 unsigned int。
由于在 decltype(x<=>f()) 中,x<=>f() 不满足“可能常量求值”的标准,因此 f 不是“用于常量求值”。 因此,不考虑 f<> 的定义是否存在,也不认为影响程序的语义。 因此,此表达式不会实例化 f<> 的定义。
因此,在原始示例中,f() 是对未定义 constexpr 函数的调用,这不是常量表达式。
根据通常的算术转换,在 x<=>f() 中,f()(类型为 int)转换为 unsigned int。当 f() 不是常量表达式时,此转换是一种缩小转换,使程序不合法。
如果 f 不是函数模板,或者其定义已被实例化,则 f() 是一个常量表达式,并且由于 f() 的结果适合 unsigned int,从 f() 到 unsigned int 的转换不是缩小转换,因此程序是合法的。

2
这是一个奇怪的地方,因为f<>的定义显然影响了这个程序的语义。 - Barry
7
@Barry:鉴于[expr.const]/15.3的脚注(https://timsong-cpp.github.io/cppwp/n4861/expr.const#footnote-81),我认为`<=>`缺失在那个项目符号列表中是措辞上的缺陷。 - Davis Herring
1
@Barry 我其实同意这很奇怪,可能是一个缺陷。但这就是规范措辞所说的。 - cpplearner
如果我们将模板函数定义为consteval,那么它会使f()成为常量表达式并使初始示例有效吗? - Fedor
1
@Fedor 我认为不会。consteval函数的未评估调用不是立即调用,因此不是“可能常量评估”。 - cpplearner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接