Static_cast security for a pointer class derived from a base destructor

This is a variant of Downcasting questions using Static_cast in C ++ and Security of invalid downcast using static_cast (or reinterpret_cast) for inheritance without adding participants

I don’t quite understand the phrase in the standard "B", which is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D "regarding the behavior in ~ B. If you discard D in ~ B, it is still a subobject in this 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 actors are an open door for disaster, and doing something similar with dtor is a bad idea, but a colleague says: β€œThe code is valid and works correctly. This is a great execution. A comment clearly indicates that it cannot be dereferenced.”

I pointed out that a throw is not needed, and we should prefer the protection provided by the type system to the comments. The sad part is that he is one of the senior / leading developers and a supposed C ++ "expert".

Can I tell him that the actor is UB?

+6
source share
3 answers

[expr.static.cast] / P11:

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

Thus, the question arises whether during static_cast pointer is actually "a B , which is actually a subobject of an object of type D ". If so, then there is no UB; if not, then undefined behavior means that the resulting pointer is dereferenced or otherwise used .

[class.dtor] / p15 says (my emphasis)

As soon as the destructor is called for the object, the object will no longer exist

and [basic.life] / p1 say that

The lifetime of an object of type T ends when:

  • if T is a class type with a nontrivial destructor (12.4), the call to the destructor begins, or
  • [...]

Thus, the lifetime of the object D ended as soon as its destructor was called, and, of course, by the time B destructor began to execute - which after the body of D finished executing the destructor. At this stage, there is no "object of type D " left, that this B can be a subobject - it "no longer exists." So you have UB.

Clang with UBsan will report an error in this code if B is made polymorphic (given the virtual function) that supports this reading.

+5
source

Apparently, your colleague is under the impression that as long as you do not play the wrong pointer, you are fine.

He is wrong .

Simple calculation of such a pointer has undefined behavior. This code is obviously broken.

+2
source

You must finally tell him that this is UB !!

Why?

12.4 / 7: Foundations and members are destroyed in the reverse order of completion of their constructor. Objects are destroyed in the reverse order of their construction.

12.6.2 / 10: The first (...) virtual base classes are initialized (...), then the direct base classes are initialized

Thus, when D is destroyed, the members of D are first destroyed, and then the D-object, and only then B will be destroyed.

This code ensures that f() is called when object B is destroyed:

  ~B() { f(this); } 

Therefore, when object D is destroyed, subproject D is first destroyed and then ~ B () is executed, calling f() .

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

3.8 / 5: (...) after the life of the object has expired, and before storage, which the object is busy, reused or freed, a pointer that refers to the storage location where the object will or was located can be used, but only in a limited way . (...) The program has undefined behavior if the pointer is used to access a non-static data element or calls the non-stationary member function object or (...) the pointer is used as the static_cast operand .

0
source

Source: https://habr.com/ru/post/983503/


All Articles