【问题标题】:Constexpr pointer to data member conversion指向数据成员转换的 constexpr 指针
【发布时间】:2019-04-25 07:47:57
【问题描述】:

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{ &amp;Derived::bar };。

constexpr 表达式(使 Clang 和 ICC 失败的那个)应该编译吗?

【问题讨论】:

  • 感谢您的回答,我已经填写了错误报告here

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


【解决方案1】:

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

data_p 指向Data 的成员foo。 derived_p 通过隐式指针指向成员转换[conv.mem]/2 指向Derived 的Data 基类子对象的成员foo。

来自[expr.static.cast]/12

“指向D 类型cv1 T 成员的指针”类型的纯右值可以转换为“指向B 类型成员的指针”类型的纯右值cv2 T”,其中B 是D 的基类,如果 cv2 与 的 cv-qualification 相同或更高 cv-qualification简历1。 […] 如果类 B 包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,行为是未定义的。 [ 注意:虽然类B不需要包含原始成员,但是通过指向成员的指针进行间接寻址的对象的动态类型必须包含原始成员;参见 [expr.mptr.oper]。 — 尾注 ]

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

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

【讨论】:

  • 这个解释怎么样? Base 是 Derived 的基类。 Derived 包含 foo(通过 Data)。遏制是否意味着“立即”遏制?
  • @YSC 但不拒绝它不是编译器的错误。 dcl.constexpr/9 说初始化器应该是一个常量表达式。当初始化程序不是常量表达式时,不诊断这种情况如何不是错误?
  • @LanguageLawyer 你缺乏上下文。我的评论已经过时了。
  • @MichaelKenzel 我同意你的观点并删除了我的答案,修复它并让两个人说同样的话是没有意义的。
  • @MichaelKenzel 注意:我认为this sentence 应该出现在你的答案中:这就是[expr.static.cast]/12 与[conv.mem]/2 的链接
【解决方案2】:

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

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

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

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

所以,gcc 也应该拒绝 sn-p,clang 和 icc 是正确的。

【讨论】:

  • @YSC 似乎我们都需要一些迭代才能在答案中直截了当:)
  • geza、MichaelKenzel 和 YSC 提出的论点让您信服吗?
猜你喜欢
  • 2018-10-09
  • 1970-01-01
  • 2013-03-10
  • 1970-01-01
  • 1970-01-01
  • 2016-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多