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

Constexpr pointer to data member conversion

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 表达式(使 Clang 和 ICC 失败的那个)应该编译吗?

我认为最后一行根本不合法,constexpr 与否。

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

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

    内的有效偏移量
  2. Base 没有 int 数据成员。无论如何,你想如何使用这个指向成员的指针?它捕获的偏移量可能引用 Derived 实例中的 Data 子对象,但这在运行时应该是 UB,在编译时应该是编译器错误。

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

我相信 GCC 和 MSVC 是正确的,这段代码应该可以编译。

data_p指向Data的成员fooderived_p 通过指向成员转换 [conv.mem]/2 的隐式指针指向 DerivedData 基 class 子对象的成员 foo

来自[expr.static.cast]/12

A prvalue of type “pointer to member of D of type cv1 T” can be converted to a prvalue of type “pointer to member of B of type cv2 T”, where B is a base class of D, if cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. […] If class B contains the original member, or is a base or derived class of the class containing the original member, the resulting pointer to member points to the original member. Otherwise, the behavior is undefined. [ Note: Although class B need not contain the original member, the dynamic type of the object with which indirection through the pointer to member is performed must contain the original member; see [expr.mptr.oper].  — end note ]

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

要初始化一个 constexpr 对象,需要一个常量表达式 [dcl.constexpr]/9. Our expression (the result of the static_cast) is a core constant expression because there is nothing in [expr.const]/2 that would say otherwise. And it is also a constant expression because it is a prvalue that satisfies all the constraints laid out in [expr.const]/5