15
votes

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.

2
Great question! Look forward to hearing the answer.SkyVar
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.Steve Jessop
@steve my code is doing the same as yours. Just that it uses a temporary variable to add clarity. Your code is attempting to do the conversion aswell.Johannes Schaub - litb
@JohannesSchaub-litb: p has type int A::*. The pointer itself doesn't need to "know" whether A is a virtual base of anything, since it is only ever dereferened by code that can figure out the A base class sub-object address before applying it. In order to have a pointer of type int B::* that referred to x, the pointer value would have to indicate that the required member is in A. That is the distinction I mean to draw by saying that you cannot have a pointer-to-member to a member in a base class.Steve Jessop
@JohannesSchaub-litb: OK, if that's how you see it :-). It's just that after the discussion with TemplateRex I came to the conclusion that the answer to your question, "why can't I do this conversion?" is "because the thing you're trying to convert to doesn't exist". Which immediately raises the new question, "why doesn't it exist?"!Steve Jessop

2 Answers

8
votes

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.

2
votes

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.