Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 585
Chris Morley Avatar asked Mar 08 '15 16:03

Chris Morley


People also ask

Can you convert a pointer from a derived class to its base class?

[19.4] Is it OK to convert a pointer from a derived class to its base class? Yes.

What will happen when a base class pointer is used to delete the derived object?

Deleting a derived class object using a pointer of base class type that has a non-virtual destructor results in undefined behavior. To correct this situation, the base class should be defined with a virtual destructor. For example, following program results in undefined behavior.

What is the use of static_cast in C++?

The static_cast operator converts variable j to type float . This allows the compiler to generate a division with an answer of type float . All static_cast operators resolve at compile time and do not remove any const or volatile modifiers.

Does a derived class need a destructor?

No. You never need to explicitly call a destructor (except with placement new). A derived class's destructor (whether or not you explicitly define one) automagically invokes the destructors for base class subobjects. Base classes are destructed after member objects.


Video Answer


2 Answers

[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.

like image 132
T.C. Avatar answered Oct 22 '22 11:10

T.C.


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.

like image 2
Lightness Races in Orbit Avatar answered Oct 22 '22 11:10

Lightness Races in Orbit