指向数据成员转换的 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
- 将行
constexpr int Derived::* derived_p{ data_p };
替换为 constexpr int Derived::* derived_p{ &Derived::bar };
。
constexpr 表达式(使 Clang 和 ICC 失败的那个)应该编译吗?
我认为最后一行根本不合法,constexpr
与否。
您可以将指向基class 成员的指针转换为指向派生class 成员的指针,但不能执行相反的操作。关于指向 class 实例本身的指针之间的转换,指针到成员的转换是 逆变。这就是为什么你需要 static_cast
来强制编译器接受这个输入,即使 Base
有一个 int
数据成员,你可以用一个指向成员的指针来引用它(见 2.下面)。
这也是有道理的:一个 Derived
是一个 Base
,因此一个 Derived
实例有一个其父对象 Base
[=49] 的子对象=].现在,指向成员的指针并不是真正的指针,它是一个 offset,只能与实际实例的地址一起使用。 Base
内的任何偏移量也是 Derived
内的有效偏移量,但 Derived
内的某些偏移量不是 Base
.
内的有效偏移量
Base
没有 int
数据成员。无论如何,你想如何使用这个指向成员的指针?它捕获的偏移量可能引用 Derived
实例中的 Data
子对象,但这在运行时应该是 UB,在编译时应该是编译器错误。
因此,gcc
也应该拒绝该代码段,clang
和 icc
是正确的。
我相信 GCC 和 MSVC 是正确的,这段代码应该可以编译。
data_p
指向Data
的成员foo
。 derived_p
通过指向成员转换 [conv.mem]/2 的隐式指针指向 Derived
的 Data
基 class 子对象的成员 foo
。
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 Base
是 Derived
的基础 class,后者包含原始成员Data::foo
在其 Data
基础 class 子对象中(上面引用中的注释似乎是支持这种解释的进一步证据)。因此,用于初始化 base_p
的 static_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。
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 失败的那个)应该编译吗?
我认为最后一行根本不合法,constexpr
与否。
您可以将指向基class 成员的指针转换为指向派生class 成员的指针,但不能执行相反的操作。关于指向 class 实例本身的指针之间的转换,指针到成员的转换是 逆变。这就是为什么你需要
static_cast
来强制编译器接受这个输入,即使Base
有一个int
数据成员,你可以用一个指向成员的指针来引用它(见 2.下面)。这也是有道理的:一个
内的有效偏移量Derived
是一个Base
,因此一个Derived
实例有一个其父对象Base
[=49] 的子对象=].现在,指向成员的指针并不是真正的指针,它是一个 offset,只能与实际实例的地址一起使用。Base
内的任何偏移量也是Derived
内的有效偏移量,但Derived
内的某些偏移量不是Base
.Base
没有int
数据成员。无论如何,你想如何使用这个指向成员的指针?它捕获的偏移量可能引用Derived
实例中的Data
子对象,但这在运行时应该是 UB,在编译时应该是编译器错误。
因此,gcc
也应该拒绝该代码段,clang
和 icc
是正确的。
我相信 GCC 和 MSVC 是正确的,这段代码应该可以编译。
data_p
指向Data
的成员foo
。 derived_p
通过指向成员转换 [conv.mem]/2 的隐式指针指向 Derived
的 Data
基 class 子对象的成员 foo
。
A prvalue of type “pointer to member of
D
of type cv1T
” can be converted to a prvalue of type “pointer to member ofB
of type cv2T
”, whereB
is a base class ofD
, if cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. […] If classB
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 classB
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 Base
是 Derived
的基础 class,后者包含原始成员Data::foo
在其 Data
基础 class 子对象中(上面引用中的注释似乎是支持这种解释的进一步证据)。因此,用于初始化 base_p
的 static_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。