根据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 的转换不是缩小转换,因此程序是合法的。
f<>
的定义显然影响了这个程序的语义。 - Barryconsteval
,那么它会使f()
成为常量表达式并使初始示例有效吗? - Fedorconsteval
函数的未评估调用不是立即调用,因此不是“可能常量评估”。 - cpplearner