7
votes

$4.11/2 states -

An rvalue of type “pointer to member of B of type cv T,” where B is a class type, can be converted to an rvalue 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, a program that necessitates this conversion is ill-formed.

My question is why we have the restriction of B not being a virtual base class of D?

3

3 Answers

5
votes

Consider a situation involving a non-virtual base class:

class A { int a; }
class B : public A { int b; }
class C : public A { int c; }
class D : public B, public C { int d; }

Here's a possible memory layout:

+-------------+
| A: int a;   |
+-------------+
| B: int b;   |
+-------------+
| A: int a;   |
+-------------+
| C: int c;   |
+-------------+
| D: int d;   |
+-------------+

D ends up with two A subobjects because it inherits from B and C and both each have an A subobject.

Pointers to member variables are typically implemented as an integer offset from the start of the object. In this case, the integer offset for int a in an A object is zero. So a "pointer to int a of type A" may be simply an integer offset of zero.

To convert a "pointer to int a of type A" to a "pointer to int a of type B," you just need an integer offset to the A subobject located in B (the first A subobject).

To convert a "pointer to int a of type A" to a "pointer to int a of type C," you just need an integer offset to the A subobject located in C (the second A subobject).

Since the compiler knows where B and C is relative to A, the compiler has enough information on how to downcast from A to B or C.

Now consider a situation involving a virtual base class:

struct A { int a; }
struct B : virtual public A { int b; }
struct C : virtual public A { int c; }
struct D : public B, public C { int d; }

Possible memory layout:

+-------------+
| B: ptr to A | ---+
|    int b;   |    |
+-------------+    |
| C: ptr to A | ---+
|    int c;   |    |
+-------------+    |
| D: int d;   |    |
+-------------+    |
| A: int a;   | <--+
+-------------+

Virtual base classes are typically implemented by having B and C (which virtually derive from A) contain a pointer to the single A subjobject. The pointers to the A subobject are required because the location of A relative to B and C is not constant.

If all we had was a "pointer to int a of type A," we won't be able to cast it to a "pointer to int a of type B", since the location of the B and C subobjects can vary relative to A. A doesn't have back-pointers to B nor C, so we simply don't have enough information for the downcast to work.

1
votes

With non-virtual inheritance, the base-class and derived-class members can be laid out contiguously in memory, with the base class first, so that the each base-class member is at the same location relative to the object's address whether the object is a B or a D. This makes it easy to convert a pointer-to-member-of-B to a pointer-to-member-of-D; both can be represented as an offset from the object's address.

With virtual inheritance, base-class members must be accessed via a pointer (or equivalent) in the derived object, indicating where the base class is located. This would require adding extra information to the pointer-to-member representation to indicate that this indirection is needed, and would necessitate a run-time check when using any pointer-to-member.

A general principle behind much of C++ is to avoid run-time overhead wherever possible. In this case, the choice was between a run-time check on quite a common operation, versus disallowing quite an obscure conversion, and it seems that that principal was applied here.

0
votes

Really interesting question. Learned something new today. This is what I could find related to the subject: Casting member function pointers from derived class to virtual base class does not work