4

We know that dynamic_cast<void*> will cast a pointer to the pointer to the most derived object; but what if the underlying object is not the most derived? For example:

class BaseClass { public: virtual void dummy() { std::cout << "Base\n"; } };

class DerivedClass : public BaseClass {
    int a{}; 
public:
    void dummy() { std::cout << "Derived\n"; } 
};
class MostDerivedClass : public DerivedClass {
    int b{}; 
public:
    void dummy() { std::cout << "Most\n"; } 
};

BaseClass* basePtr_d = new DerivedClass, *basePtr_md = new MostDerivedClass;
DerivedClass* derivedPtr = 
    dynamic_cast<DerivedClass*>(basePtr_d); // right
MostDerivedClass* mostDerivedPtr = 
    dynamic_cast<MostDerivedClass*>(basePtr_md); // right
MostDerivedClass* mostDerivedPtr2 = 
    static_cast<MostDerivedClass*>(dynamic_cast<void*>(basePtr_md)); // right
DerivedClass* derivedPtr2 = 
    static_cast<DerivedClass*>(dynamic_cast<void*>(basePtr_d)); // What happens??

What happens for the last case?

o_oTurtle
  • 1,091
  • 3
  • 12
  • I was wondering the same thing after watching a talk on reflection. – alfC Mar 16 '23 at 11:09
  • My understanding of dynamic polymorphism is shaky (almost never use it). But according to my experiment, dynamic_cast returns a non-null void pointer. What can you do with it? Can someone give a non-trivial example, showing observable *behavior*? https://godbolt.org/z/qPEsG8757 – alfC Mar 16 '23 at 11:16
  • @alfC Recently I read an article about analyzing the performance of `dynamic_cast`, and the author reaches the conclusion that it may vary a lot in different cases; it may be as fast as `static_cast`(when it comes to `dynamic_cast`) or dramatically slow in some other cases. Maybe this can make `dynamic_cast` less "shaky" to some degree. Sorry but [this article](https://lancern.xyz/2022/11/04/dynamic-cast-benchmark/) is in Chinese, so the automatic translation may not be satisfactory :). – o_oTurtle Mar 16 '23 at 11:26
  • [These answers](https://stackoverflow.com/q/4131091/17017616) are very related. – leosch Mar 16 '23 at 11:30
  • 3
    @alfC You can use the resulting void pointer to check for object identity. E.g. if you have two pointers to some types in a polymorphic class hierarchy, you can use `dynamic_cast` on both and compare the resulting pointers to check if your two pointers actually point to the same object. – chrysante Mar 16 '23 at 11:37
  • 1
    @alfC `dynamic_cast` is used for deallocation, as it gives a pointer to the most derived object at the start of an allocation – Artyer Mar 16 '23 at 11:48
  • I understand the identity and the deallocation. What I don't understand is what it means to be a pointer to the most derived type if there is no type information in the result. Are we talking about offsets? – alfC Mar 16 '23 at 12:19
  • Also, for deallocation, polymorphic or not, deallocation ends up converting any pointer to void* (or char* conceptually). I don't see what is special. Destruction is another story but I don't see the connection. – alfC Mar 16 '23 at 12:21
  • 3
    I don't understand the issue. "to the most derived object" doesn't mean the "most derived" class that exists in the program; that would be massively ambiguous anyway. It just means "the actual concrete type of the object in question". `basePtr_d` points to a `DerivedClass`. So the void pointer will correctly point to the full `DerivedClass` object and can be safely cast to `DerivedClass*`. – Sebastian Redl Mar 16 '23 at 13:04
  • @SebastianRedl Yeap, I think this is ambiguous too, so I ask this question. After all, there seems to be no definition of "most derived object" in the C++ standard. – o_oTurtle Mar 16 '23 at 13:21
  • 1
    @o_oTurtle It's in N4928 (latest draft right now) 6.7.2p6: "If a complete object, a member subobject, or an array element is of class type, its type is considered the *most derived class*; [...]; an object of a most derived class type or of a non-class type is called a *most derived object*". – Sebastian Redl Mar 16 '23 at 13:26
  • @SebastianRedl I think that's quite reasonable! I may accept personally if you answer this question, but I'm not pretty sure whether others regard it as right because the wording here in the standard is still a little bit oblique. BTW, the standard entry mentioned can be found [here](https://eel.is/c++draft/intro.object#6) for others' interests. – o_oTurtle Mar 16 '23 at 14:36
  • @o_oTurtle The standard defines the term "most derived object", and then uses that definition wherever it needs to. Don't assume that any particular term in the standard is just plain english. – Caleth Mar 17 '23 at 09:52

3 Answers3

1

The last case is the same as the previous case. You have a BaseClass *, you do a pointer conversion which gives you the value of a pointer to the most derived object, and then you statically cast that value to the appropriate type. At no point does the type MostDerivedClass get involved in these conversions.

"most derived class" is not a global property of the types in your program. It refers to the particular object you have a pointer or reference to. For objects that you get from new, it is the type that was used in the new expression.

basePtr_d points to the BaseClass subobject of a DerivedClass instance, and basePtr_md points to the BaseClass subobject of a MostDerivedClass instance.

Caleth
  • 52,200
  • 2
  • 44
  • 75
0

The rule is:

In dynamic_cast<new-type>(expression), if expression is a pointer to a polymorphic type, and new-type is a pointer to void, the result is a pointer to the most derived object pointed or referenced by expression. See dynamic_cast #4.

It's easy to miss the important distinction: the pointer type and the pointer value, the later being sometimes referred to as the dynamic type of a pointer.

When B derives from A, a pointer of type A* and a pointer of type B* both pointing to the same object of type B might have a different values. The restriction on dynamic_cast<void*> covers this specificity and guarantees a return value for the most derived type, even though the static type of the returned pointer is of course void*. This is still true if there exist a type C deriving from B or A.

YSC
  • 38,212
  • 9
  • 96
  • 149
0

All the static_cast from void* does is change the type without changing the value of the pointer. It is the same as a reinterpret_cast. In this case everything should be fine, because the MostDerivedClass object has the same address as its DerivedClass subobject.

If this were not the case however, derivedPtr2 would be an invalid pointer. Technically this might still be UB, I'm not sure on that though. Say your MostDerivedClass actually looks like this:

struct X { int i; };

class MostDerivedClass : public X, public DerivedClass {
    int b{}; 
public:
    void dummy() { std::cout << "Most\n"; } 
};

Now doing a static_cast from MostDerivedClass* to DerivedClass* changes the value of the pointer, possibly by adding an offset of four bytes. Now this line

DerivedClass* derivedPtr2 = 
    static_cast<DerivedClass*>(dynamic_cast<void*>(basePtr_d)); // What happens??

statically casts a void pointer (which points to the MostDerivedClass object or also the X subobject) to DerivedClass*, which is essentially a reinterpret_cast. So you end up with a pointer of type DerivedClass which actually points to the X-subobject, not to the DerivedClass subobject. So the derivedPtr2 pointer you end up with is invalid.

chrysante
  • 2,328
  • 4
  • 24