7

Most discussion I've seen about member pointers focus on conversions permitted on the type to which the member belongs. My question is about conversions on the type of the member.

struct Base{};
struct Derived : public Base{};
struct Foo{ Derived m_Derived; };

Given these declarations, the following code produces an error (MSVC 2008):

// error C2440: 'initializing' : cannot convert from 'Derived Foo::* ' to 'Base Foo::* '
Base Foo::*p = &Foo::m_Derived;

Conversion from Derived* to Base* is normally allowed - why the difference here?

Chris
  • 71
  • 2
  • 1
    "Conversion from Base* to Derived* is normally allowed" I think you have this backward. – John Dibling Dec 09 '10 at 22:33
  • See http://stackoverflow.com/questions/4295117/pointer-to-member-conversion – icecrime Dec 09 '10 at 22:57
  • 1
    That question's analog in this question would be Foo and some subclass of it; quite different from what is here, I think. – lijie Dec 09 '10 at 23:27
  • 1
    Unearthed this discussion from 1999: http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/da6937df55e9b11a – Chris Dec 10 '10 at 16:26

3 Answers3

2

You have the variance backward.

Return types convert implicitly to base types (contravariance). But parameters convert implicitly to derived types (covariance), and the class type in a pointer-to-member acts as a parameter. To see this, let's apply the Liskov Substitutability Principle:

The contract of Base* is: "I will give you a Base" (when you use the * operator on me). The contract of Derived* is "I will give you a Derived, which is also a Base".

Clearly a Derived* can be used in place of a Base*. Therefore there is an implicit conversion from Derived* to Base*.

But consider the contract of a pointer-to-member.

The contract of int Base::* is: "Give me a Base and I will give you back an int" (A Derived is a Base, so those are ok too) The contract of int Derived::* is: "Give me a Derived and I will give you back an int" (But not any old Base will do, it must be a Derived)

Imagine that you have a Base which is not a Derived. It will work nicely when dereferencing an int Base::*, but cannot be used with an int Derived*).

However, if you have a Derived, you can use it to dereference both int Base::* and int Derived::*. Therefore there is an implicit conversion from int Base::* to int Derived::*

Argh, I did what you said and analyzed the type that the member belongs to.

The LSP still works though. And I agree that the conversion should be legal, at least according to type safety. The contract is "Give me a Foo and I will give you a Derived", which clearly you can use to get from Foo to Base by composing it with an implicit conversion. So it's safe. DeadMG is probably on the right track pointing out the potential complication with the relation location of the base subobject, especially in virtual inheritance. But pointer-to-members deal with these problems on the LHS of the dereference operator, so they could for the result as well.

Final answer is probably simply that the standard doesn't require that the conversion is legal.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Strangely, `Derived *(Foo::*)` _can_ be converted to `Base *(Foo::*)` (by comeau). I wonder if this is an "extension"? – lijie Dec 10 '10 at 06:13
1

@Ben Voigt: Why did you strike out your correct answer?

Safe pointer conversions are contra-variant, meaning, you can upcast safely but not downcast.

Safe pointer to member conversions are co-variant, meaning you can downcast safely, but not upcast.

The reason is easy to see when you think about it. Suppose you have a pointer to a member of Base, that's an offset into Base. Then if the complete object is a Derived, what's the offset of that same member into Derived? Usually its exactly the same offset: certainly it will be if the pointers to Base and Derived would be equal addresses. If you have multiple inheritance and virtual bases things get a bit trickier :)

In fact, the OP's example code is the perfect example of why up casts are not safe: the offset of a member of Derived class could be applied to any Base, and point off the end of the base subobject, which is only OK if it is actually a Derived and not say, a Derived2.

Yttrill
  • 4,725
  • 1
  • 20
  • 29
  • 1
    But we aren't trying to upcast the class of the data pointer, we are trying to point a data pointer to an upcast of its type (I'm lacking names here...). These aren't the same thing. – Hexagon Dec 10 '10 at 06:05
  • There's nothing that says a pointer must be a simple offset, and further, OP's question is not about a "pointer to a member of Base". It is about a "pointer to a member of Foo" with the member type being Base. And the compiler _does_ take into account converting a "pointer to a member of A" to a "pointer to a member of B" where A is derived from B. – lijie Dec 10 '10 at 06:09
  • Because my original answer, like yours, talks about members of `Derived` and members of `Base`. But the question is about members typed `Base` or typed `Derived`, but all are members **of** `Foo`. – Ben Voigt Dec 10 '10 at 06:32
0

Interesting question. Data pointers are so rarely used, I'm quite unfamiliar with the rules.

However, I'm going to put money that it's because of multiple inheritance. If Base and Derived use virtual inheritance, the compiler can't know at compile-time the offset of Base within any given Derived, making a compile-time offset impossible, and it would be far too much work to legalize it for non-virtual inheritance.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • but the same argument works against allowing conversions for the non-member pointers, doesn't it? – lijie Dec 09 '10 at 22:48
  • @lijie: No, because those conversions occur at runtime. This conversion is effectively happening at compile-time. – Puppy Dec 09 '10 at 23:05
  • But nothing says that the conversion has to happen at compile time[?] – lijie Dec 10 '10 at 06:03
  • @lijie: It is a compile-time conversion, because how else could a member data pointer possibly be implemented? It is an offset from "this", and you ask the compiler for it, which means that it must be constant. How can the compiler make a run-time offset, when you have no object? – Puppy Dec 10 '10 at 12:46
  • 1
    But `T (A::*)` can be converted to `T (B::*)`, even when `B` is virtually derived from `A`. If the member data pointer is just an offset from `this`, how can such a conversion be implemented? – lijie Dec 10 '10 at 12:59