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 int Derived::* derived_p{ data_p };
行替换为constexpr int Derived::* derived_p{ &Derived::bar };
。应该编译constexpr表达式(使Clang和ICC失败的表达式)吗?
答案 0 :(得分:3)
我相信GCC和MSVC是正确的,应该编译此代码。
data_p
指向foo
的成员Data
。 derived_p
通过指向成员转换[conv.mem]/2的隐式指针指向foo
的{{1}}基类子对象的成员Data
。
类型“指向 cv1
Derived
的{{1}}成员的指针的prvalue可以转换为类型为{{1} },其类型为 cv2D
,其中,如果 cv2 具有相同的cv资格,则T
是B
的基类与 cv1 相同或更高的cv资格。 […] 如果类T
包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针将指向原始成员。否则,行为是不确定的。 [注意:尽管类B
不必包含原始成员,但是通过指向成员的指针执行间接访问的对象的动态类型必须包含原始成员;请参阅[expr.mptr.oper]。 —尾注]
如@geza在下面的评论中所指出的,类D
是B
的基类,后者的{{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
与否。
您可以将指向基类成员的指针转换为指向派生类成员的指针,但是不能做相反的操作。关于指向类实例本身的指针之间的转换,指针到成员的转换是不变的。这就是为什么即使static_cast
有一个Base
数据成员(您可以使用指向该成员的指针来引用)的原因,也需要int
来强制编译器接受此输入的原因(请参见2。下面)。
这也很有意义:Derived
是-Base
,因此Derived
实例具有其父Base
类的子对象。现在,指向成员的指针实际上不是指针,而是一个 offset ,仅可用于实际实例的地址。 Base
内的任何偏移量也是Derived
内的有效偏移量,但Derived
内的某些偏移量不是Base
内的有效偏移量。
Base
没有int
数据成员。无论如何,您将如何使用该指针指向成员?它捕获的偏移量可能引用Data
实例中的Derived
子对象,但这在运行时应该是UB,在编译时应该是编译器错误。
因此,gcc
也应拒绝该代码段,clang
和icc
对此是正确的。