GCC 8.2.1 和 MSVC 19.20 编译了以下代码,但 Clang 8.0.0 和 ICC 19.0.1 无法编译。
// Base class.
struct Base {};
// Data class.
struct Data { int foo; };
// Derived class.
struct Derived : Base, Data { int bar; };
// Main function.
int main()
{
constexpr int Data::* data_p{ &Data::foo };
constexpr int Derived::* derived_p{ data_p };
constexpr int Base::* base_p{ static_cast<int Base::*>(derived_p) };
return (base_p == nullptr);
}
Clang 8.0.0 的错误消息如下:
case.cpp:16:33: error: constexpr variable 'base_p' must be initialized by a constant expression
constexpr int Base::* base_p{ static_cast<int Base::*>(derived_p) };
~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我注意到它在两种情况下与 Clang 编译良好:
- 从最后一个定义中删除 constexpr
- 将行
constexpr int Derived::* derived_p{ data_p };
替换为constexpr int Derived::* derived_p{ &Derived::bar };
。
constexpr 表达式(使 Clang 和 ICC 失败的表达式)应该编译吗?
我相信GCC和MSVC是正确的,这段代码应该编译。
data_p
指向Data
的成员foo
。derived_p
通过指向成员转换 [conv.mem]/2 的隐式指针指向Derived
的Data
基类子对象的成员foo
。
来自 [expr.static.cast]/12
类型为"指向cv1
T
类型D
的成员的指针"的 prvalue 可以转换为"指向 cv2T
类型B
成员的指针"类型的 prvalueB
,其中 是D
的基类,如果 cv2 与cv1具有相同的 cv 资格,或比cv1具有更高的cv资格。[...]如果类B
包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,行为是未定义的。[ 注意:尽管类B
不需要包含原始成员,但通过指向成员的指针执行间接寻址的对象的动态类型必须包含原始成员;请参阅 [expr.mptr.oper]。 — 尾注 ]
正如@geza在下面的评论中指出的那样,类Base
是Derived
的基类,后者在其Data
基类子对象中包含原始成员Data::foo
(上面引用中的注释似乎是支持这种解释的进一步证据)。因此,用于初始化base_p
的static_cast
格式正确,并且具有定义良好的行为。生成的指针从Derived
对象的Base
基类子对象的角度指向该Derived
对象的Data::foo
成员。
要初始化constexpr
对象,需要一个常量表达式 [dcl.constexpr]/9。我们的表达式(static_cast
的结果)是一个核心常量表达式,因为 [expr.const]/2 中没有任何内容可以说其他情况。它也是一个常量表达式,因为它是一个满足 [expr.const]/5 中列出的所有约束的 prvalue。
我认为最后一行根本不合法,无论是否constexpr
。
指针转换为指向派生类成员的指针,但不能执行相反的操作。关于指向类实例本身的指针之间的转换,指针到成员的转换是逆变的。这就是为什么您需要
static_cast
来强制编译器接受此输入的原因,即使Base
具有int
数据成员,您可以使用指向成员的指针引用该成员(请参阅下面的 2.)。这也是有道理的:
Derived
是Base
,因此Derived
实例具有其父类Base
子对象。现在,指向成员的指针并不是真正的指针,它是一个偏移量,只能与实际实例的地址一起使用。Base
内的任何偏移量也是Derived
内的有效偏移量,但Derived
内的某些偏移量不是Base
内的有效偏移量。Base
没有int
数据成员。无论如何,您希望如何使用此指向成员的指针?它捕获的偏移量可能是指Derived
实例中的Data
suboject,但这在运行时应该是UB,在编译时应该是编译器错误。
因此,gcc
也应该拒绝该代码段,clang
和icc
是正确的。