指向数据成员转换的 Constexpr 指针



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的成员fooderived_p通过指向成员转换 [conv.mem]/2 的隐式指针指向DerivedData基类子对象的成员foo

来自 [expr.static.cast]/12

类型为"指向cv1T类型D的成员的指针"的 prvalue 可以转换为"指向 cv2T类型B成员的指针"类型的 prvalueB,其中 是D的基类,如果 cv2 与cv1具有相同的 cv 资格,或比cv1具有更高的cv资格。[...]如果类B包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,行为是未定义的。[ 注意:尽管类B不需要包含原始成员,但通过指向成员的指针执行间接寻址的对象的动态类型必须包含原始成员;请参阅 [expr.mptr.oper]。 — 尾注 ]

正如@geza在下面的评论中指出的那样,类BaseDerived的基类,后者在其Data基类子对象中包含原始成员Data::foo(上面引用中的注释似乎是支持这种解释的进一步证据)。因此,用于初始化base_pstatic_cast格式正确,并且具有定义良好的行为。生成的指针从Derived对象的Base基类子对象的角度指向该Derived对象的Data::foo成员。

要初始化constexpr对象,需要一个常量表达式 [dcl.constexpr]/9。我们的表达式(static_cast的结果)是一个核心常量表达式,因为 [expr.const]/2 中没有任何内容可以说其他情况。它也是一个常量表达式,因为它是一个满足 [expr.const]/5 中列出的所有约束的 prvalue。

我认为最后一行根本不合法,无论是否constexpr

可以将指向基类成员的
  1. 指针转换为指向派生类成员的指针,但不能执行相反的操作。关于指向类实例本身的指针之间的转换,指针到成员的转换是逆变的。这就是为什么您需要static_cast来强制编译器接受此输入的原因,即使Base具有int数据成员,您可以使用指向成员的指针引用该成员(请参阅下面的 2.)。

    这也是有道理的:DerivedBase,因此Derived实例具有其父类Base子对象。现在,指向成员的指针并不是真正的指针,它是一个偏移量,只能与实际实例的地址一起使用。Base内的任何偏移量也是Derived内的有效偏移量,但Derived内的某些偏移量不是Base内的有效偏移量。

  2. Base没有int数据成员。无论如何,您希望如何使用此指向成员的指针?它捕获的偏移量可能是指Derived实例中的Datasuboject,但这在运行时应该是UB,在编译时应该是编译器错误。

因此,gcc也应该拒绝该代码段,clangicc是正确的。

相关内容

  • 没有找到相关文章

最新更新