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 cvT
”, whereB
is a class type, can be converted to a prvalue of type “pointer to member ofD
of type cvT
”, whereD
is a derived class (Clause 10) ofB
. IfB
is an inaccessible (Clause 11), ambiguous (10.2), or virtual (10.1) base class ofD
, or a base class of a virtual base class ofD
, a program that necessitates this conversion is ill-formed.
Both function and data member pointers are affected.
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 throughpa
withinfoo()
, since the actual type ofpa
can vary with each offoo()
's invocations. Rather, the compiler must transform the code doing the access so that the resolution ofX::i
can be delayed until runtime.
Essentially, the presence of a virtual base class invalidates bitwise copy semantics.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With