具有部分定义 class 的模板化 constexpr 函数调用会更改后续结果

Templated constexpr function invocation with partially defined class changes subsequent results

我有一个带有多个静态函数重载的结构,以一些 counter<int> 作为参数:

struct S {
    static void fn(counter<1>);
    static void fn(counter<2>);
    static void fn(counter<3>);
};

模板化的 constexpr 函数可用于查找特定的重载 class:

template <typename T>
inline constexpr size_t count_fns() {
    // 'defines_fn' is a type trait, full code in demo link
    if constexpr (defines_fn<T, counter<99> >::value) { return  99; }
    if constexpr (defines_fn<T, counter<98> >::value) { return  98; }
    if constexpr (defines_fn<T, counter<97> >::value) { return  97; }
    // [...]
    if constexpr (defines_fn<T, counter<3> >::value) { return  3; }
    if constexpr (defines_fn<T, counter<2> >::value) { return  2; }
    if constexpr (defines_fn<T, counter<1> >::value) { return  1; }
    return 0;
}

正常使用,count_fns<S>() returns 3,符合预期。

但是,添加调用 count_fns 的内联静态变量会改变一些事情:

struct U {
    static void fn(counter<1>);
    static void fn(counter<2>);
    static constexpr size_t C0 = count_fns<U>();  // C0 is 2
    static void fn(counter<3>);
};

static_assert(count_fns<U>() == 3, " <-- fails, value is actually 'still' 2");

Godbolt 建议此行为在编译器(MSVC、gcc、clang)中是一致的: Demo

这是预料之中的,还是 constexpr 解释器的某种未定义行为?


这些是counterdefines_fn的定义:

template <size_t Value>
struct counter {
    static constexpr size_t value = Value;
};

template <typename T, typename Arg, class = void>
struct defines_fn { static constexpr bool value = false; };

template <typename T, typename Arg>
struct defines_fn<T, Arg, std::void_t<decltype(T::fn(std::declval<Arg>()))> >
{
    static constexpr bool value = true;
};

应该管理此类代码的标准部分是 [temp.point]. Unfortunately, it's considered to be defective. CWG 287

标准在技术上说,在您的示例中,在 U 的定义中引用了 count_fns<U>,实例化点被认为是在 U 的定义之后.然而,这会导致荒谬;例如,如果我们在 U:

中有以下声明,会发生什么
static void fn(counter<2 * C0>);

现在看来我们有一个循环依赖。

CWG 287 关注 class 模板专业化。这些问题有点不同,因为 [temp.point] 说(例如)如果您要在 U 的定义中引用 class 模板特化,那么实例化点将定义U之前。 (我还没有弄明白为什么 class 模板和函数模板的规则不同。)

为了解决这两种情况下的问题,“常识”方法似乎是从 class 定义中引用的模板和 non-template 构造都应该能够看到先前声明的成员class 的(这适用于函数和 class 模板特化)。 (如果引用它们的上下文是 complete-class 上下文,它们应该能够看到 class 的所有成员。这可能吗?是否会导致其他问题?我不是当然可以。所以我现在要避免这个问题。)

遵循该原则,如果 C0 的初始化程序需要模板特化,编译器似乎会将实例化点放在 C0 声明之前或之后。在实现之前或之后存在差异,但无论如何,它都在 C0 之前的成员声明之后,以及 C0 之后的成员声明之前。所有主要的编译器似乎都同意这一点。

count_fns<U> 的第二个实例化,在 static_assert 声明中,引用了与第一个实例化相同的 defines_fn class 模板特化集。 Class 模板特化,不像函数模板特化,每次被引用时都不会 re-instantiated; first 实例化被“缓存”。请参阅 [temp.point] p4 和 p7。所以第二次调用 count_fns<U> returns 与第一次调用的结果相同。

所以这似乎就是您看到您所看到的行为的原因。在 CWG 287 确定之前,我们不能说它是对还是错。