使用派生类的静态constexpr数据成员来初始化基类的静态constexpr数据成员

16

Consider the following code:

template<typename T>
struct S { static constexpr int bar = T::foo; };

struct U: S<U> { static constexpr int foo = 42; };

int main() { }

GCC v6.1编译它,clang 3.8则报错:

2 : error: no member named 'foo' in 'U'
struct S { static constexpr int bar = T::foo; };

哪个编译器是正确的?
这可能是因为在我们尝试在S中使用U时,它不是一个完整的类型
在这种情况下,这应该被认为是GCC的错误,但在搜索/打开错误跟踪器之前,我想知道是否正确...

编辑

同时我已经向GCC提交了错误
等待其接受答案。


1
最新的标准工作草案更新批次包括对[9.2.3.2p3]的更改,涉及到内联变量的新概念(constexpr变量现在与函数一样隐式地内联),因此C++17的答案可能会改变;目前的答案仍适用于C++14及以下版本。我会等待最新版本的规范在官方邮件中发布,然后我会使用特定于C++17的信息更新答案。 - bogdan
@bogdan 哇,非常感谢。真的很感激。 - skypjack
@bogdan 哇,谢谢!不幸的是我不能给你点赞两次,但我可以接受这个答案,因为我之前忘记了。抱歉并再次感谢。 - skypjack
没问题。干杯! - bogdan
1个回答

10
对于C++14和11,Clang是正确的;然而,在最新的工作草案(未来的C++17)中,情况已经发生了变化-请参见下一节。
要查找的标准引用是(来自N4140,最接近C++14的草案):

[temp.inst]/1:

隐式实例化类模板特化会导致声明的隐式实例化,但不会导致类成员函数、成员类、作用域成员枚举、静态数据成员和成员模板的定义、默认参数或异常规范的隐式实例化。请注意保留HTML标签。

[temp.point]/4:

对于类模板的特化,[...] 特化的实例化点紧接在引用该特化的命名空间作用域声明或定义之前。
因此,对于 S<U> 的实例化点就在 U 的声明之前,只有一个前向声明 struct U; 在概念上被插入,以便找到名称 U

[class.static.data]/3:

根据上面引用的段落,可以在类定义中使用constexpr限定符声明字面类型的静态数据成员;如果这样做,它的声明应该指定一个大括号或等号初始化器,其中每个是赋值表达式初始化器子句都是常量表达式。如果程序中使用了odr-used(3.2)并且命名空间作用域定义不包含初始化器,则仍然需要在命名空间作用域中定义该成员。根据以上引用的段落,即使在S的定义中具有初始化器,bar的声明仍然只是一个声明而不是定义,因此当隐式实例化S<U>时,它被实例化,并且此时没有U::foo

一种解决方法是将bar变成一个函数;根据第一个引用,该函数的定义不会在隐式实例化S<U>时被实例化。只要在看到U的定义之后使用bar(或从S的其他成员函数体内,因为这些函数体只有在需要时才会单独实例化 - [14.6.4.1p1]),类似以下代码就可以工作:

template<class T> struct S 
{
   static constexpr int bar() { return T::foo; }
};

struct U : S<U> { static constexpr int foo = 42; };

int main()
{
   constexpr int b = U::bar();
   static_assert(b == 42, "oops");
}

在工作草案(目前为N4606)中采纳了P0386R2后,[class.static.data]/3已被修改; 相关部分现在如下:

[...] 内联静态数据成员可以在类定义中定义并指定花括号或等号初始化器。如果使用constexpr说明符声明该成员,则可以在命名空间范围内重新声明而不带有初始化器(此用法已被弃用,请参见D.1)。[...]

这与[basic.def] / 2.3的更改相辅相成:

除非声明了一个类定义中的非内联静态数据成员(9.2、9.2.3),否则声明是一个定义

[...]

因此,如果它是内联的,则为定义(带或不带初始化程序)。[dcl.constexpr]/1说:
[...] 用constexpr说明符声明的函数或静态数据成员隐式地是内联函数或变量(7.1.6)。[...]
这意味着bar的声明现在是一个定义,并且根据上一节中的引用,在S<U>的隐式实例化中不会被实例化;只有bar的声明,在那个时候不包括初始化程序,被实例化。
在当前工作草案中的[depr.static_constexpr]中,这种情况的变化在示例中得到了很好的总结:
struct A {
   static constexpr int n = 5; // definition (declaration in C++ 2014)
};

const int A::n; // redundant declaration (definition in C++ 2014)

这使得GCC在C++1z模式下的行为符合标准。

我认为不需要插入任何U的前向声明,也不需要:S不需要查找名称 U,只需要查找作为typedef-name的名称T,它是class U的一个。 - Davis Herring
@DavisHerring 概念上插入。这只是一种生成特化的思考方式;编译器不一定要按照这种方式进行,但这是一种与标准规则一致的思考点实例化的方式(当然,还要考虑特殊的依赖名称查找规则等)。我并不是自己想出来的,我曾见过编译器编写者使用这个模型来讨论这个问题。 - bogdan
即使您更喜欢使用T作为typedef名称,在生成每个特化之前不断重新定义它的概念,以定义该别名,您仍需要在其之前可见的东西的声明来定义它所别名的内容,因此您仍需要类U的前向声明。 - bogdan
那些特殊的依赖名称规则对于一个函数 f,如果它的地址被用作模板参数在自己的定义中被引用时是否“声明”早期是敏感的。如果模板通过依赖ADL尝试查找 f,但 f 尚未被声明,则它是不合法的NDR。对于一个类来说,这可能并不重要,尽管我不同意 typedef-name 的目标必须存在声明;考虑 using X=decltype(new int***);,其中 X 命名了一个从未声明过也从未使用过的类型。 - Davis Herring
@DavisHerring 是的,因此“允许特殊的依赖名称查找规则等”。这个概念声明只是一种简单的方式来可视化替换的模板参数,仅此而已。它不应被视为影响其他任何事情(如果它像真实声明一样行为,我可以想到至少一个牵强的情况也会影响类名,但这不是重点)。关于别名,“它所代表的东西”在这种情况下是class U;当然我们可以找到其他不涉及声明的结构,但我认为它们与我们的情况无关。 - bogdan

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