【问题标题】:Why is it disallowed to convert from VirtualBase::* to Derived::*?为什么不允许从 VirtualBase::* 转换为 Derived::*?
【发布时间】:2014-04-18 01:38:44
【问题描述】:

昨天,我和我的同事不确定为什么语言禁止这种转换

struct A { int x; };
struct B : virtual A { };

int A::*p = &A::x;
int B::*pb = p;

连演员表都没有帮助。如果基成员指针是虚拟基类,为什么标准不支持将基成员指​​针转换为派生成员指针?

相关C++标准参考:

“指向 cv T 类型的成员的指针”类型的纯右值,其中B 是类类型,可以转换为“指向D 类型的成员的指针”类型的纯右值cv T”,其中DB 的派生类(第10 条)。如果BD 的不可访问(第11 条)、模糊(10.2)或虚拟(10.1)基类,或D 的虚拟基类的基类,则需要此转换的程序是格式不正确。

函数和数据成员指针都会受到影响。

【问题讨论】:

  • 好问题!期待听到答案。
  • 在与 TemplateRex 讨论之后,这个问题是否可以简化为“为什么我不能做int B::*pb = &B::x;?不仅仅是你不能转换p:你不能有指针-to-member 到虚拟基地中的成员。
  • @steve 我的代码和你的一样。只是它使用一个临时变量来增加清晰度。您的代码也在尝试进行转换。
  • @JohannesSchaub-litb:p 的类型为 int A::*。指针本身不需要“知道”A 是否是任何东西的虚拟基础,因为它只会被可以在应用之前找出A 基类子对象地址的代码解引用。为了有一个指向xint B::* 类型的指针,指针值必须表明所需的成员在A 中。这就是我想说的区别,你不能有一个指向成员的指针,指向基类中的成员。
  • @JohannesSchaub-litb:好的,如果你是这样看的:-)。只是在与 TemplateRex 讨论之后,我得出的结论是,您的问题的答案是,“为什么我不能进行这种转换?”是“因为您要转换的东西不存在”。这立即提出了一个新问题,“它为什么不存在?”!

标签: c++ virtual-inheritance member-pointers


【解决方案1】:

简答

我相信即使Derived 实际上派生自Base,编译器也可以将Base::* 转换为Derived::*。为此,指向成员的指针需要记录的不仅仅是偏移量。它还需要通过某种类型擦除机制来记录原始指针的类型。

所以我的猜测是委员会认为这对于一个很少使用的功能来说太过分了。此外,使用纯库功能可以实现类似的功能。 (见长答案。)

长答案

我希望我的论点在某些极端情况下没有缺陷,但我们开始吧。

本质上,指向成员的指针记录了成员相对于类开头的偏移量。考虑:

struct A { int x; };
struct B : virtual A { int y; };
struct C : B { int z; };

void print_offset(const B& obj) {
  std::cout << (char*) &obj.x - (char*) &obj << '\n';
}

print_offset(B{});
print_offset(C{});

在我的平台上,输出是1216。这表明a相对于obj的地址的偏移量取决于obj的动态类型:12如果动态类型是B16如果它是C

现在考虑 OP 的例子:

int A::*p = &A::x;
int B::*pb = p;

正如我们所见,对于静态类型B 的对象,偏移量取决于其动态类型,并且在上面的两行中,没有使用B 类型的对象,因此没有动态类型可以从中获取偏移量。

但是,要取消引用指向成员对象的指针是必需的。编译器不能获取当时使用的对象来获得正确的偏移量吗?或者,换句话说,偏移量计算是否可以延迟到我们评估obj.*pb(其中obj 是静态类型B)?

在我看来这是可能的。将obj 转换为A&amp; 并使用pb 中记录的偏移量(从p 读取)来获得对obj.x 的引用就足够了。为此,pb 必须“记住”它是从 int A::* 初始化的。

这里是一个模板类ptr_to_member 的草稿,它实现了这个策略。专业化ptr_to_member&lt;T, U&gt; 应该与T U::* 类似地工作。 (请注意,这只是一个可以通过不同方式改进的草案。)

template <typename Member, typename Object>
class ptr_to_member {

  Member Object::* p_;
  Member& (ptr_to_member::*dereference_)(Object&) const;

  template <typename Base>
  Member& do_dereference(Object& obj) const {
      auto& base = static_cast<Base&>(obj);
      auto  p    = reinterpret_cast<Member Base::*>(p_);
      return base.*p;
  }

public:

  ptr_to_member(Member Object::*p) :
    p_(p),
    dereference_(&ptr_to_member::do_dereference<Object>) {
  }

  template <typename M, typename O>
  friend class ptr_to_member;

  template <typename Base>
  ptr_to_member(const ptr_to_member<Member, Base>& p) :
    p_(reinterpret_cast<Member Object::*>(p.p_)),
    dereference_(&ptr_to_member::do_dereference<Base>) {
  }

  // Unfortunately, we can't overload operator .* so we provide this method...
  Member& dereference(Object& obj) const {
    return (this->*dereference_)(obj);
  }

  // ...and this one
  const Member& dereference(const Object& obj) const {
    return dereference(const_cast<Object&>(obj));
  }
};

它的使用方法如下:

A a;
ptr_to_member<int, A> pa = &A::x; // int A::* pa = &::x
pa.dereference(a) = 42;           // a.*pa = 42;
assert(a.x == 42);

B b;
ptr_to_member<int, B> pb = pa;   // int B::* pb = pa;
pb.dereference(b) = 43;          // b*.pb = 43;
assert(b.x == 43);

C c;
ptr_to_member<int, B> pc = pa;   // int B::* pc = pa;
pc.dereference(c) = 44;          // c.*pd = 44;
assert(c.x == 44);

很遗憾,单独ptr_to_member 并不能解决Steve Jessop 提出的问题:

在与 TemplateRex 讨论之后,这个问题是否可以简化为“为什么我不能做 int B::*pb = &B::x;?这不仅仅是你不能转换 p:你不能有一个指向成员的指针,指向虚拟基中的成员。

原因是表达式&amp;B::x 应该只记录xB 开头的偏移量,正如我们所见,这是未知的。为了完成这项工作,在意识到B::x 实际上是虚拟基础A 的成员之后,编译器需要从&amp;A::X 创建类似于ptr_to_member&lt;int, B&gt; 的东西,它“记住”在构造时看到的A时间并记录xA开头的偏移量。

【讨论】:

  • 不错。 reinterpret_cast 合法吗?如果不是,我认为根本不重要,因为如果需要,你可以想出另一种类型擦除的方法。
  • @SteveJessop 我认为是:在构造函数中,它从Member Base::* 转换为Member Object::*。结果存储在p_ 中。 dereference_p_ 的唯一用户)设置为 do_dereference&lt;Base&gt;,这会将 p_ 转换回原始类型。 AFAIK 这可以通过 5.2.10/9 第二个要点:“将类型的右值“指针到类型为 T1 的 X 的数据成员”转换为类型“指向类型为 T2 的 Y 的数据成员”(其中对齐要求T2 的不比 T1 更严格)并且返回到其原始类型会产生指向成员值的原始指针。”这里T1 = T2 = Member
【解决方案2】:

Lippman 的“Inside the C++ Object model”对此进行了讨论:

[there] 需要将虚拟基类的位置设置在 每个派生类对象在运行时可用。例如,在 以下程序片段:

class X { public: int i; }; 
class A : public virtual X { public: int j; }; 
class B : public virtual X { public: double d; }; 
class C : public A, public B { public: int k; }; 
// cannot resolve location of pa->X::i at compile-time 
void foo( const A* pa ) { pa->i = 1024; } 

main() { 
 foo( new A ); 
 foo( new C ); 
 // ... 
} 

编译器无法修复通过X::i访问的物理偏移量 pafoo() 中,因为pa 的实际类型可能随每个 foo() 的调用。相反,编译器必须转换代码 进行访问,以便X::i 的分辨率可以延迟到 运行时。

本质上,虚拟基类的存在使按位复制无效 语义

【讨论】:

  • 但是类成员在内存中不是通过继承排序的吗?例如。 [X members][A or B members][C members]。在这种情况下,无论对象实际上是什么,尝试访问 X::sth(理论上)都应该是确定性的。
  • @Spook 的重点是编译器需要灵活性,因为A: virtual X 可以与B : virtual X 组合成C : A, B。缺少整体程序分析,固定布局很快就会发生冲突。 Lippman 有一句名言:“虚拟基类支持飘入拜占庭”。
  • 为什么编译器不能用成员指针做同样的“让我们在运行时获得精确的偏移量”?
  • @JohannesSchaub-litb 我不确定,也许采用virtual 基类的类与virtual 函数的组合太复杂而无法可靠地计算子对象布局。至少编译器供应商在标准确立时是这样认为的(李普曼的书是 1996 年出版的)。
  • @JohannesSchaub-litb btw,也许dynamic_cast 可以与足够智能的编译器一起工作?它不会是免费的,因为虚拟基类可以添加额外的间接级别来跟踪偏移量。
猜你喜欢
  • 2014-12-07
  • 1970-01-01
  • 2013-04-23
  • 2015-03-12
  • 2021-10-09
  • 2011-04-26
  • 2020-01-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多