15

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

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

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

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

相关C++标准参考:

“指向Bcv 类型成员的指针”类型的纯右值T,其中B是类类型,可以转换为“指向Dcv 类型成员的指针”类型的纯右值T,其中D是 的派生类(第 10 条)B。如果B是 的不可访问(第 11 条)、模棱两可 (10.2) 或虚拟 (10.1) 基类D,或 的虚拟基类的基类D,则需要进行此转换的程序格式错误。

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

4

2 回答 2

8

Lippman 的“ C++ 对象模型内部”对此进行了讨论:

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

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通过 pawithin访问的物理偏移量foo(),因为 的实际类型pa可能随每个 foo()'s 调用而变化。相反,编译器必须转换执行访问的代码,以便可以将解析X::i延迟到运行时。

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

于 2014-03-13T11:26:12.153 回答
2

简短的回答

我相信即使实际上Base::*从. 为此,指向成员的指针需要记录的不仅仅是偏移量。它还需要通过某种类型擦除机制记录原始指针的类型。Derived::*DerivedBase

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

长答案

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

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

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{});

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

现在考虑 OP 的例子:

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

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

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

在我看来,这是可能的。obj转换并使用记录的A&偏移量pb(从 中读取p)来获取对obj.x. 为此,pb必须“记住”它是从int A::*.

ptr_to_member这是实现此策略的模板类草案。专业化ptr_to_member<T, U>应该与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:你不能有一个指针 - to-member 到虚拟基地中的成员。

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

于 2014-03-14T00:10:38.867 回答