Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it disallowed to convert from VirtualBase::* to Derived::*?

Yesterday, me and my colleague weren't sure why the language forbids this conversion

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

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

Not even a cast helps. Why does the Standard not support converting a base member pointer to a derived member pointer if the base member pointer is a virtual base class?

Relevant C++ standard reference:

A prvalue of type “pointer to member of B of type cv T”, where B is a class type, can be converted to a prvalue of type “pointer to member of D of type cv T”, where D is a derived class (Clause 10) of B. If B is an inaccessible (Clause 11), ambiguous (10.2), or virtual (10.1) base class of D, or a base class of a virtual base class of D, a program that necessitates this conversion is ill-formed.

Both function and data member pointers are affected.

like image 482
Johannes Schaub - litb Avatar asked Mar 13 '14 11:03

Johannes Schaub - litb


2 Answers

Lippman's "Inside the C++ Object model" has a discussion about this:

[there] is the need to make the virtual base class location within each derived class object available at runtime. For example, in the following program fragment:

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 ); 
 // ... 
} 

the compiler cannot fix the physical offset of X::i accessed through pa within foo(), since the actual type of pa can vary with each of foo()'s invocations. Rather, the compiler must transform the code doing the access so that the resolution of X::i can be delayed until runtime.

Essentially, the presence of a virtual base class invalidates bitwise copy semantics.

like image 70
TemplateRex Avatar answered Nov 13 '22 10:11

TemplateRex


Short answer:

I believe a compiler could make conversion from Base::* to Derived::* possible even when Derived derives virtually from Base. For this to work a pointer to member would need to record more than just the offset. It would also need to record the type of the original pointer through some type-erasure mechanism.

So my speculation is that the committee thought that this would be too much for a feature that is rarely used. In addition, something similar can be achieved with a pure library feature. (See the long answer.)

Long answer:

I hope my argument is not flawed in some corner case but here we go.

Essentially a pointer to member records the offset of the member with respect to the beginning of the class. Consider:

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

On my platform the output is 12 and 16. This shows that the offset of a with respect to obj's address depends on obj's dynamic type: 12 if the dynamic type is B and 16 if it's C.

Now consider the OP's example:

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

As we saw, for an object of static type B, the offset depends on its dynamic type and in the two lines above no object of type B is used so there's no dynamic type to get the offset from.

However, to dereference a pointer to member an object is required. Couldn't a compiler take the object used at that time to get the correct offset? Or, in other words, could the offset computation be delayed until the time we evaluate obj.*pb (where obj is of static type B)?

It seems to me that this is possible. It's enough to cast obj to A& and use the offset recorded in pb (which it read from p) to get a reference to obj.x. For this to work pb must "remember" that it was initialized from an int A::*.

Here is a draft of template class ptr_to_member that implements this strategy. The specialization ptr_to_member<T, U> is supposed to work similarly to T U::*. (Notice this is just a draft that can be improved in different ways.)

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

Here is how it should be used:

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

Unfortunately, ptr_to_member alone doesn't solve the issue raised by Steve Jessop:

Following discussion with TemplateRex, could this question be simplified to "why can't I do int B::*pb = &B::x;? It's not just that you can't convert p: you can't have a pointer-to-member to a member in a virtual base at all.

The reason is that the expression &B::x is supposed to record only the offset of x from the beginning of B which is unkown as we have seen. To make this work, after realising that B::x is actually a member of the virtual base A, the compiler would need to create something similar to ptr_to_member<int, B> from &A::X which "remembers" the A seen at construction time and records the offset of x from the beginning of A.

like image 2
Cassio Neri Avatar answered Nov 13 '22 10:11

Cassio Neri