Pointer-to-member standard conversion sequence
Asked Answered
T

2

7

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:

  • Are the above explanations entirely true?

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.

Topsyturvydom answered 21/3, 2022 at 19:5 Comment(0)
A
2

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.

Araby answered 21/3, 2022 at 20:11 Comment(1)
"but the only way to use it would be to cast it back to a D2::*": The way the standard words it it seems to be fine to apply such a member pointer directly to an expression of the base class type if the dynamic type of the expression contains the referenced member.Avaria
A
2

Your confusion arises from a misconception that &B::a, &D2::a and &D1::a are distinct. In fact, they are identical; they are each prvalues of type int B::*, all designating &B::a. They are precisely as identical as 1 is identical to 1x and 01, or as, given inline namespace ns { int j; }, &ns::j is identical to &j.

You can observe this by compiling a simple program:

template<auto> int g();
int f1() { return g<&B::a>(); } // calls int g<&B::a>() 
int f2() { return g<&D2::a>(); } // calls int g<&B::a>() 
int f3() { return g<&D1::a>(); } // calls int g<&B::a>() 

The best way to understand a pointer to (data) member is as a function (in the foundational sense); a pointer of type T M::* (say) maps glvalues of type M to glvalues of type T. In the simple case, where the designated data member is at a fixed offset within the class, a PTDM may be implemented as a byte offset (in the sense of offsetof), but in more complex cases (involving virtual inheritance) it may need to consult the vtable.

Now, why is &B::a, of type int B::*, implicitly convertible to int D1::*? The answer is that a pointer D1* is implicitly convertible to B* (by derived-to-base conversion, since D1 has an unambiguous accessible base of type B), and pointers to data member are contravariant with their class types. In other words, given a function from glvalues of type B to glvalues of type int, we can always construct a function from glvalues of type D1 to glvalues of type int, by prepending the derived-to-base conversion: (D1 → B) ∘ (B → int)D1 → int.

So:

  1. int D1::*ptr = &B::a; is OK by implicit conversion (from int B::* to int D1::*.
  2. int D1::*ptr = &D2::a; is OK for the exact same reason, since &D2::a is just an obfuscated name for &B::a.
  3. int B::*ptr = &D1::a; is OK because &D1::a is just an obfuscated name for &B::a.
  4. int D1::*ptr = &D2::b; fails because &D2::b is genuinely of type int D2::*, not of type int B::*.

You could force #4 to compile by casting via int B::*; you can explicitly downcast a pointer to data member for the same reason you can explicitly upcast a pointer to class: you're telling the compiler to trust you, that this pointer to data member of the derived class is actually a converted pointer to data member of the base class. Since the explicit upcast static_cast<D2*> on a pointer of type B* is allowed, the effect of applying the downcasted pointer to data member is that of upcasting an instance pointer from B* to D2* and then accessing D2::b.

That is, given p1 of type D1*, p1->*static_cast<int D1::*>(static_cast<int B::*>(&D2::b)) is equivalent to static_cast<D2*>(static_cast<B*>(p))->*(&D2::b), i.e. to static_cast<D2*>(static_cast<B*>(p))->b, and likewise has undefined behavior.

Finally, why does the compiler decide that &D1::a should be of type int B::* and not int D1::*? This is the language doing you a favor; a pointer to data member of base class is more powerful than a pointer to data member of derived class, since it can be applied to any derived class (with accessible unambiguous base). The only situation where you would want to convert would be where you can access the base class and you want to give the pointer to someone who can't, i.e. private or protected inheritance.

Augmenter answered 22/3, 2022 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.