2

In C++ ABI implementations modeled after the Itanium C++ ABI, which is followed by many ABIs for other processors, virtual destructors actually occupy two vtable slots. Besides the "complete object destructor", which does what you would expect, there is a second entry for the "deleting destructor", which calls the first, and then deletes the memory of the object.

There is a problem with this approach, which can be a nuisance in small memory systems: The dynamic memory manager is linked in, even when no other code uses it. This is dead code when there is no call to delete anywhere in the application. This is because the C++ compiler/linker usually isn't able to detect that a slot in the vtable isn't called from anywhere, and hence to remove the associated code. Clearly, it would be better if the deleting destructor could be implemented in a different way that doesn't involve a vtable entry, and allows the compiler/linker to omit this dead code.

One can of course implement a custom void operator delete(void *) {} to prevent the linker from bringing in the dynamic memory code, but this still doesn't prevent the deleting destructor code to be emitted entirely.

Hence my question: Is there no better way to implement deleting destructors? My idea would have been to return the pointer to the start of the memory block to delete from the complete object destructor. If the memory block is to be deleted after destruction, this returned address can be used by a nonvirtual function that calls operator delete. Essentially, having the memory address returned by the complete object destructor would allow the deleting destructor to be nonvirtual, and therefore eligible for dead code elimination.

But I guess I must have overlooked something, which makes that rather simple solution impossible. But what would that be? Can someone expound the the design decision in the Itanium ABI for me?

Edit: I have found information that provides a partial answer here:

The top answer contains this explanation:

When some class defines its own operator delete, the selection of a specific operator delete to call is done as if it was looked up from inside the class destructor. The end result of that is that for classes with virtual destructor operator delete behaves as if it were a virtual function (despite formally being a static member of the class).

Apparently, the way chosen by the Itanium API to make it behave like a virtual function, is to make the destructor that calls it an actual virtual function.

However, that is not the only way to implement it. The linked article centers around an implementation that uses a single virtual function with a hidden parameter, but that solution produces the same undesirable behaviour I was describing above. Another implementation might be to have the complete object destructor return the address of the operator delete() if there is a custom implementation for the class, and nullptr otherwise. This would avoid the problem I described above.

So, in a somewhat modified form, my question still stands.

sh-
  • 941
  • 6
  • 13
  • May be a dup: https://stackoverflow.com/questions/6613870/gnu-gcc-g-why-does-it-generate-multiple-dtors – Eljay Feb 25 '20 at 18:30
  • That's not a dup, but still thanks for the link. I don't ask why there are multiple destructors, but why the deleting destructor needs to be virtual, which has some undesirable consequences, as described above. – sh- Feb 26 '20 at 11:03

1 Answers1

1

Unlike normal virtual functions at destruction of a class, one has to call its destructor and destructors of all its parents in the correct order.

It is technically possible to unite all the function calls into a single function call but it would require knowledge of the implementation of each destructor or full structure of the object. Basically de-virtualizing the call. Compilers aren't good at it. Not too sure on all the details as it is quite complex question considering all the possible overly-virtual objects.

ALX23z
  • 4,456
  • 1
  • 11
  • 18
  • The deleting destructor calls the complete object destructor and then calls `operator delete()`. So it is the complete object destructor that does all the work of calling the destructors of subobjects. I don't see how the deleting destructor has anything to do with that. As far as I can see, the reason for making the deleting destructor virtual is that the correct address must be found for passing it to operator delete(), and that can only be determined by the (derived) class that is being destroyed. But if the complete object destructor returns the address, the problem disappears, right? – sh- Feb 26 '20 at 11:10
  • @sh- no the complete object destructor calls only the next one in line and each one only knows the next one to be called. How else to organize it when some of destructors's implementation is hidden from the final derived class? – ALX23z Feb 26 '20 at 11:36
  • The compiler knows all base classes and subobjects when compiling the complete object destructor, so it knows all destructors. However, you are right in one respect: Since a base class destructor calls its own base class destructors, the derived class destructor doesn't follow the derivation chain, but only calls the destructors of its own direct bases. – sh- Feb 26 '20 at 11:53
  • @sh- no the compiler doesn't know either constructors or destructors if the implementation is hidden away in a different .cpp. Furthermore, it could've been a different compiler version/brand that built the constructor/destructors implementation and organized things differently - but it will still work if they follow same basic rules regarding ABI. Perhaps, what you want is possible but it restricts ABI compatibility. Also, why worry about amount of memory that virtual classes require? You shouldn't be creating too many virtual classes in the first place templates provide better solution. – ALX23z Feb 26 '20 at 12:08
  • The compiler doesn't know the code of the constructors or destructors, but it does know their name and therefore knows how to call them. I don't see why the code of a constructor/destructor needs to be seen by the compiler for anything I was interested in. Note that I'm not trying to inline all destructor code into a single function. I'm trying to find out why the deleting destructor needs to be in the vtable. – sh- Feb 26 '20 at 12:23
  • @sh- it is required because of the decision that each destructor calls the next one in line. Next destructor to be called can be determined only by the final derived class. Thus next destructor needs to be in vtable. Also complete object destructor needs to be in vtable else one wouldn't be able to destroy. That's 2. Blame the extra memory for the decision to execute destructor as series of chain calls. – ALX23z Feb 26 '20 at 12:59
  • I think you are having it wrong here. See for example the description in https://en.cppreference.com/w/cpp/language/destructor Chapter "Destruction sequence". Knowing the code of a destructor is nowhere needed for that. – sh- Feb 26 '20 at 13:27
  • @sh- I read but the question is how the Destruction Sequence is implemented. It test on MSVC and here I see only a single pointer for the destructor in vtable. Apparently indeed compiler traces the whole thing and calls destructors one by one. But it could be also implemented via "virtual next destructor call". Changing it is an ABI break. You can try checking what purpose this second destructor has by creating various odd classes. – ALX23z Feb 26 '20 at 14:39
  • MSVC apparently passes a bool to the destructor in the vtable to distinguish between the two cases, similar to what was described for older gcc versions in the second link I posted above. You may use https://godbolt.org to verify this. As I explained, the MSVC method uses one less vtable slot, but otherwise suffers from the same problem I described. – sh- Feb 26 '20 at 15:13
  • @sh- now I am confused. If you have a single function for destructor in vtable then what's the problem? It needs to have at least one - one cannot have 0. – ALX23z Feb 26 '20 at 15:38
  • Read my original question again, please. The original problem isn't how many destructors there are in the vtable, it is the code they link to. If I don't use dynamic memory allocation, I don't want the memory management code to be linked in. The deleting destructor calls the memory deallocation functions and thereby brings in dead code, which the linker can't remove when the deleting destructor is in the vtable. The only other way this can be solved is with a linker that can detect which vtable slots are being called, and set the ones to nullptr which aren't. – sh- Feb 26 '20 at 16:28
  • @sh- if you build a shared library then there is no escape. If you build a static library then code that doesn't participate in compilation is unused - or at least shouldn't be. One cannot just remove dynamic memory support simply because it is unused. – ALX23z Feb 26 '20 at 16:37
  • @sh- in any case, you can simply remove virtual from destructors and they won't take any extra vtable space. – ALX23z Feb 26 '20 at 16:50
  • The "small memory systems" I was referring to are predominantly embedded devices, i.e. microcontrollers. There is often no need nor support for shared libraries. The entire application is statically linked. If I don't use dynamic memory allocation, I don't want the code for it linked in. However, I may still want to use virtual functions, because of their runtime dispatch. Virtual functions should be orthogonal with dynamic memory allocation in C++. – sh- Feb 26 '20 at 17:14
  • @sh- all you can do is remove virtual destructors from your code. You cannot remove it completely unless you don't use classes with virtual destructors at all. – ALX23z Feb 26 '20 at 18:45
  • I realize that, which is why I asked for a rationale why the C++ ABI was defined that way. Not having a virtual destructor in a class with virtual functions is possible, fortunately. One would make the nonvirtual destructor protected in this case, to avoid disaster. If the base classes can be designed this way, this would be my preferred workaround. – sh- Feb 26 '20 at 19:28