I used to always copy and paste my code in cppinsights to see where's in my code a conversation is performed implicitly by the compiler. But pointer-to-member standard conversion is not shown on this site.
I am getting more confused when seeing the answer to this question: Downcasting pointer to member leads to undefined behavior. I think the question itself is asked wrongly since, per my knowledge, a pointer-to-member can be safely downcasted, per [conv.mem]/2; but it cannot be upcasted safely except [expr.static.cast]/12 is applied. This question has the following code (edited):
struct B { int a; };
struct D1 : B {};
struct D2 : B { int b; };
void f()
{
int D1::*ptr = &B::a; // #1
int D1::*ptr = &D2::a; // #2
int B::*ptr = &D1::a; // #3
int D1::*ptr = &D2::b; // #4
}
My confusion arises just because I don't know to-what actually ptr is pointing? to the member of the enclosing object itself? or to the member of the subobject of the enclosing object?
Hence, before posting any questions here, I am turned to a some C++ programmer (actually my retired teacher) to help me and explain the above implicit conversations (if any). And briefly that's what I am getting:
#1- Here, the initializer is a prvalue designating a pointer-to-member B::a. And because the D1 has B unnamed subobject, ptr can point to the a member of that subobject; in fact, ptr points to D1::B::a, where B::a is a member of B subobject of D1. The resulting pointer ptr refers to the same member of the subobject B (not D1) which is actually D1::B::a; that's because even if D1 has an actual not-inherited member named a, the resulting pointer still refers to the same member of that subobject.
#2- Here, the initializer is a prvalue designating a pointer-to-member D2::a. And because the D2 (like D1) has B unnamed subobject, ptr can point to the a member of that subobject; in fact, ptr points to D1::B::a, where B::a is a member of B subobject of D1. The resulting pointer ptr refers to the same member of the subobject B (not D1) which is actually D1::B::a that's because even if D1 has an actual not-inherited member named a, the resulting pointer still refers to the same member of that subobject.
#3- Here, the initializer is a prvalue designating a pointer-to-member D1::a. And because the D1 has B unnamed subobject, ptr can point to the a member of that subobject; in fact, ptr points to D1::B::a, where B::a is a member of B subobject of D1. The resulting pointer ptr refers to the same member of the subobject B (not D1) which is actually D1::B::a; that's because even if D1 has an actual not-inherited member named a, the resulting pointer still refers to the same member of that subobject.
#4- Here, the initializer is a prvalue designating a pointer-to-member D2::b. And because the D2 has no D1 subobject can ptr pointer points to its D1::a member, the program necessitates this conversion is ill-formed even if static_cast is used.
Actually, I feel that this explanation is logical and satisfies me, but I think that it has some mistakes and is not 100% true, specifically, paragraph #3.
My Question:
Please feel free to correct the paragraphs if they have some mistakes, and answer with the corrected paragraphs, if possible. Thanks, and sorry for taking long.
I would advise thinking of a pointer to a member as an offset, not really a pointer at all.
So the pointer to member tells you what offset to use to get to a particular member within an object--but to dereference it, you have to combine the pointer to member with the base address of an object of the appropriate type.
That means a pointer to member, on its own, doesn't really point to anything. It (at least normally) specifies an adjustment/offset that can be applied to the address of an actual object to get to the specified member of that object.
Most of your first three questions really boil down to how name lookup is done. When you give the name of a member, the compiler looks up the name, and figures out what part of the object that name refers to. The pointer to member than holds whatever offset is necessary to get to that member of an instance of that object, or any object derived from that one (which gets tricky in one case (I'll get into below, but that you didn't ask about).
So, in your first three cases, in any of B, D1, or D2, there's only one member named a. So when the name a is looked up, it's going to refer to the a that's a member of the base class, regardless of how you designated it, and the pointer to member is going to hold the same offset.
Your fourth case simply shouldn't compile as it stands now. With a static cast, you can make it compile, but the only way to use it would be to cast it back to a D2::* (and then apply it to an object of type D2 or something derived from it).
Above I mentioned an interesting case you didn't cover. That's when you get multiple inheritance involved. In this case, the compiler may need to apply two different offsets when you refer to a member of a base class object.
struct B1 { int a; };
struct B2 { int b; };
struct D1 : public B1, public B2 {};
int B1::*ptr1 = &B1::a;
int B2::*ptr2 = &B2::b;
D1 d;
d.*ptr1 = 1;
d.*ptr2 = 2;
In this case, ptr1 and ptr2 each contain the offset of a and b in their respective classes (normally going to be 0 in both cases).
But when we apply then to an object of D1 (which has both B1 and B2 base classes) the compiler has to first apply one offset to get to the base class sub-object, then apply the second pointer to member offset to get to the member of that base sub-object.
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