When may the dynamic type of a referred to object

2019-07-20 17:22发布

问题:

Let us begin with an example:

#include <cstdio>

struct Base { virtual ~Base() {} virtual void foo() = 0; };
struct P: Base { virtual void foo() override { std::printf("Hello, World!"); } };
struct N: Base { virtual void foo() override {} };

void magic(Base& b);
// Example implementation that changes the dynamic type
// {
//     void* s = dynamic_cast<void*>(&b);
//     b.~B();
//     new (s) N();
// }

int main() {
    std::aligned_storage<sizeof(P), alignof(P)> storage;
    void* s = static_cast<void*>(storage);

    new (s) P();

    Base& b = *static_cast<Base*>(s);

    magic(b);

    b.foo();
 }

What, according to the Standard, should b.foo() print ?

Personal opinion: it's undefined because b got stale after we destroyed the instance in magic. In this case, would replacing b.foo() by static_cast<B*>(s)->foo() make it legal ?


So now that we have an example that may (or not) be legal, the more general question at hand for all of us standardistas is whether changing the dynamic type of an object is ever allowed. We already know that the C++ compiler may reuse storage (fortunately), so it's a bit tricky.

The question might seem theoretical, however it has immediate application for compilers: may the compiler devirtualize b.foo() to b.P::foo() in the program above ?

And therefore I am looking for:

  • a definite answer regarding my own little program (I could not come up with one).
  • a possible example (a single would suffice) of a legal way of changing the dynamic type of an object.

回答1:

According to §8.5.3.2 of the standard, a reference cannot be bound to another object after initialisation. Since placement new creates a new object, you are violating that rule, and get undefined behaviour.

The dynamic type of an object cannot change. Even in your example, you're not changing the type of an object, but creating a new different object in the same place as the old one. If you think about it, changing the dynamic type of an object would imply resizing the object in-place to accommodate extra data members and changing the VMT (and then that would move other objects and screw up pointers...) which can't be done within the rules of the language.



回答2:

It's undefined behaviour. Your magic example is violating the semantics of a reference.

Also, dynamic_cast is for down-casting. Cast to void* is a static_cast.

To answer your questions explicitly:

  • A compiler may "devirtualize" any function call it likes, if it can prove the runtime type.
  • If a reference outlives the object it refers, it's UB.
  • You cannot change the dynamic type of an object, the closest thing you can do is re-assign a pointer.

    Base * ptr;
    P p;
    N n;
    
    ptr = &p; ptr -> foo ();
    ptr = &n; ptr -> foo ();
    

But p and n are of fixed type until they go out of scope (or, if allocated on the heap, when they are deleted).