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

时间:2019-04-25 07:47:57

标签: c++ language-lawyer constexpr pointer-to-member

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失败的表达式)吗?

2 个答案:

答案 0 :(得分:3)

我相信GCC和MSVC是正确的,应该编译此代码。

data_p指向foo的成员Dataderived_p通过指向成员转换[conv.mem]/2的隐式指针指向foo的{​​{1}}基类子对象的成员Data

来自[expr.static.cast]/12

  

类型“指向 cv1 Derived的{​​{1}}成员的指针的prvalue可以转换为类型为{{1} },其类型为 cv2 D,其中,如果 cv2 具有相同的cv资格,则TB的基类与 cv1 相同或更高的cv资格。 […] 如果类T包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针将指向原始成员。否则,行为是不确定的。 [注意:尽管类B不必包含原始成员,但是通过指向成员的指针执行间接访问的对象的动态类型必须包含原始成员;请参阅[expr.mptr.oper]。 —尾注]

如@geza在下面的评论中所指出的,类DB的基类,后者的{{1 }}基类的子对象(上面引用中的Note似乎是支持这种解释的进一步证据)。因此,用于初始化B的{​​{1}}格式正确,行为明确。从该Base对象的Derived基类子对象的角度来看,结果指针指向Data::foo对象的Data成员。

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

答案 1 :(得分:1)

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

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

    这也很有意义:Derived是-Base,因此Derived实例具有其父Base类的子对象。现在,指向成员的指针实际上不是指针,而是一个 offset ,仅可用于实际实例的地址。 Base内的任何偏移量也是Derived内的有效偏移量,但Derived内的某些偏移量不是Base内的有效偏移量。

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

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