Chris Morley
Chris Morley

Reputation: 901

Safety of static_cast to pointer-to-derived class from base destructor

This is a variant of the questions Downcasting using the Static_cast in C++ and Safety of invalid downcast using static_cast (or reinterpret_cast) for inheritance without added members

I am not clear on the phrase in the standard "B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D" with respect to behavior in ~B. If you cast to D in ~B, is it still a subobject at that point ? The following simple example shows the question:

void f(B* b);

class B {
public:
  B() {}
  ~B() { f(this); }
};

class D : public B { public: D() {} };

std::set<D*> ds;

void f(B* b) {
  D* d = static_cast<D*>(b);  // UB or subobject of type D?
  ds.erase(d);
}

I know that the cast is an open door to disaster, and doing anything like this from the dtor is a bad idea, but a co-worker claims "The code is valid and works correctly. That cast is perfectly valid. The comment clearly states that it should not be dereferenced".

I pointed out that the cast is unnecessary and we should prefer the protection provided by the type system to comments. The sad part is that he is one of the senior/lead developers and a supposed c++ "expert".

Can I tell him the cast is UB ?

Upvotes: 6

Views: 1756

Answers (3)

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385144

Apparently your co-worker is under the impression that as long as you do not dereference an invalid pointer, you are fine.

He is wrong.

Merely evaluating such a pointer has undefined behaviour. This code is obviously broken.

Upvotes: 2

Christophe
Christophe

Reputation: 73376

You should definitively tell him that it's UB ! !

Why ?

12.4/7: Bases and members are destroyed in the reverse order of the completion of their constructor The objects are detroyed in the reverse order of their constuction.

12.6.2/10: First (...) virtual base classes are initialized (...) then, direct base classes are initialized

So when destructing a D, first the D members are destructed and then the D sub object, and only then will B be destructed.

This code makes sure that f() is called when the B object is destroyed:

 ~B() { f(this); } 

So when a D object is destroyed, the D suboject is destroyed first, and then ~B() is executed, calling f().

In f() you cast the pointer to a B as pointer to a D. This is UB:

3.8/5: (...) after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. (...) The program has undefined behavior if the pointer is used to access a non-static data member or call a non-static member function of the object, or (...) the pointer is used as the operand of a static_cast.

Upvotes: 0

T.C.
T.C.

Reputation: 137330

[expr.static.cast]/p11:

A prvalue of type “pointer to cv1 B,” where B is a class type, can be converted to a prvalue of type “pointer to cv2 D,” where D is a class derived (Clause 10) from B, if a valid standard conversion from “pointer to D” to “pointer to B” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is neither a virtual base class of D nor a base class of a virtual base class of D. The null pointer value (4.10) is converted to the null pointer value of the destination type. If the prvalue of type “pointer to cv1 B” points to a B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the behavior is undefined.

The question, then, is whether, at the time of the static_cast, the pointer actually points to "a B that is actually a subobject of an object of type D". If so, there is no UB; if not, then the behavior is undefined whether or not the resulting pointer is dereferenced or otherwise used.

[class.dtor]/p15 says that (emphasis mine)

Once a destructor is invoked for an object, the object no longer exists

and [basic.life]/p1 says that

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
  • [...]

From this, then, the D object's lifetime has ended as soon as its destructor is invoked, and certainly by the time B's destructor began to execute - which is after D's destructor body has finished execution. At this point, there is no "object of type D" left that this B can be a subobject of - it "no longer exists". Thus, you have UB.

Clang with UBsan will report an error on this code if B is made polymorphic (given a virtual function), which supports this reading.

Upvotes: 5

Related Questions