使用函数参数作为常量表达式的一部分 - gcc vs clang

Using function argument as part of a constant expression - gcc vs clang

考虑以下代码片段:

template <bool> struct B { };

template <typename T>
constexpr bool pred(T t) { return true; } 

template <typename T>
auto f(T t) -> decltype(B<pred(t)>{})
{
}

尽管 g++ 的诊断具有误导性,但我认为这里的问题是 t 不是 常量表达式 。将代码更改为...

decltype(B<pred(T{})>{})

...修复了 g++ 上的编译错误:live example on godbolt.org


哪个编译器在这里运行正确?

编译器期望在该上下文中有一个参数,因为它需要评估完整的(模板重载的)函数类型。考虑到 pred 的实现,任何值都可以在那个位置工作。这里它将 f 参数的模板类型绑定到参数。 g++ 编译器似乎在做一个简化的假设,即模板 constexpr 函数将以某种方式被任何参数改变,除非它们也是 const,正如你所证明的那样,clang 同意,不是必然如此。

这一切都归结于编译器在函数实现内部的深度,由于对 return 值的非常量贡献,编译器将该函数标记为非常量。

然后是函数是否被实例化并要求编译器实际编译代码与执行模板解析的问题,至少对于 g++,这似乎是不同级别的编译。

然后我去了标准,他们好心地允许编译器作者做出准确的简化假设,模板函数实例化应该只适用于 f<const T>f <const T&>

constexpr` functions must have: each of its parameters must be LiteralType

因此模板代码应该可以编译,但如果用非常量 T 实例化则会失败。

t 不是 constexpr 值,这意味着 pred(t) 也不是 constexpr。 你不能在 B<pred(t)> 中使用它,因为这需要 constexpr.

此版本编译正确:

template <bool> struct B { };

template <typename T>
constexpr bool pred(T t) { return true; } 

template <typename T, T t>
auto f() -> decltype(B<pred(t)>{})
{
}

https://godbolt.org/g/ydbj1X

另一个有效代码是:

template <typename T>
auto f(T t) -> decltype(pred(t))
{
}

这是因为您不评估 pred(t) 只有您获得类型信息。 B<pred(t)> 需要 pred(t) 的评估,否则您将获得 B<true>B<false>,对于您无法达到的任何正常值。

std::integral_constant<int, 0>{} 可以在 Clang 案例中工作可能是因为它的值作为类型的一部分构建并且始终相同。如果我们稍微更改一下代码:

template <typename T>
auto f(T t) -> decltype(B<pred(decltype(t){})>{})
{
    return {};
}

Clang 和 GCC 都会编译它,如果 std::integral_constanttdecltype(t){} 总是具有相同的值。

GCC 是错误的。 没有规则阻止以这种方式在常量表达式中使用函数的参数。

但是,您不能在这样的上下文中使用参数的 f 可调用的类型集 T 是相当有限。要了解原因,我们需要考虑在计算表达式 pred(t):

时将计算哪些构造
// parameters renamed for clarity
template <typename U>
constexpr bool pred(U u) { return true; } 

template <typename T>
auto f(T t) -> decltype(B<pred(t)>{});

调用 pred(t) 的求值语义如下:

  1. f的参数t
  2. 复制初始化pred的参数u
  3. 评估 pred 的主体,这会简单地创建一个 booltrue
  4. 摧毁u

因此,f 仅可用于类型 T,上面仅涉及在常量评估期间有效的构造(有关规则,请参阅 [expr.const]p2)。要求是:

  • T 必须是文字类型
  • t 复制初始化 u 必须是常量表达式,特别是,不得对 t 的任何成员执行左值到右值的转换(因为它们的值是未知的),并且不得命名 t
  • 的任何引用成员

实际上,这意味着如果 T 是具有默认复制构造函数的空 class 类型,或者如果 T 是 class 类型,其复制构造函数是 constexpr 并且不读取其参数的任何成员,或者(奇怪的是)如果 Tstd::nullptr_t(尽管 clang currently gets the nullptr_t case wrong)。