Why is static downcasting unique_ptr unsafe?
Asked Answered
J

1

8

I am referring to a follow-up question to "Downcasting" unique_ptr<Base> to unique_ptr<Derived> which seems to make sense to me in itself.

OP asks for getting a unique_ptr<Derived> out of a unique_ptr<Base>, where the latter object is of dynamic type Derived, so that the static downcast would be safe.

Normally, (as 95% of solutions in the Internet suggest, roughly estimated), the simple solution would be:

unique_ptr<Derived> ptr(static_cast<Derived*>(baseClassUniquePtr.release()));

OP also states, though

PS. There is an added complication in that some of the factories reside in DLLs that are dynamically loaded at run-time, which means I need to make sure the produced objects are destroyed in the same context (heap space) as they were created. The transfer of ownership (which typically happens in another context) must then supply a deleter from the original context. But aside from having to supply / cast a deleter along with the pointer, the casting problem should be the same.

Now, the solution seems to be to get the deleter from the unique_ptr<Base> object and pass it to the new object, which clearly results in unique_ptr<Derived, default_delete<Base>>. But default_delete is stateless anyways. The only difference is the template argument. But since we always declare dtor virtual when using inheritance with dynamic polymorphism in C++, we always call ~Derived anyways, so I would think, the original deleter would be safe anyways, allowing for a clean cast to unique_ptr<Derived> (without unhandy second template argument which forbids any usual storage).

So, while I understand that we have two heap spaces when using a library (DLL, .dylib, ...) that creates an object and passes it to some executable, I do not understand how copying/moving a stateless deleter from the old object solves this issue.

Does it even solve the issue? If yes, how? If not, how can we solve this issue?

--- Edit: Also... get_deleter returns a reference to an object lying in the old unique_ptr which is destroyed when this old unique_ptr is destroyed, is it not? --- (stupid question because unique_ptr along with deleter is clearly moved)

Jenifferjenilee answered 10/10, 2019 at 8:37 Comment(4)
The address of the delete function not the destructor of the object may be different when you have more than 1 heap space.Biconvex
I don't see any problem with OP's code from the referenced question. The situation when one want to transfer deleter as well may arise when that deleter is cutom cleanup function.Baeda
@RichardCritten Does this mean that a stateless object where a delete function is called somewhere stores the address of the delete function?Jenifferjenilee
@Jenifferjenilee Kind of a contradiction yes/no? I'm not sure (going to have a look). I think MS fixed this a while back by directly calling WinAPI heap functions. However it is possible to create a second heap (and custom allocator(s)) and this may reintroduce the problem.Biconvex
G
1

Does it even solve the issue?

You're right, it doesn't (on its own). But that isn't because the default deleter is stateless, but because its implementation is inline.

If not, how can we solve this issue?

We have to make sure that the call to delete happens from the module from which the object was originally allocated (let's call it module A). Since std::default_delete is a template, it is instantiated on-demand and an inline version is called from module B. Not good.


Method 1

One solution is to use a custom deleter all the way through. It doesn't have to be stateful, as long as its implementation resides in module A.

// ModuleA/ModuleADeleter.h

template <class T>
struct ModuleADeleter {
    // Not defined here to prevent accidental implicit instantiation from the outside
    void operator()(T const *object) const;
};

// Suppose BUILDING_MODULE_A is defined when compiling module A
#ifdef BUILDING_MODULE_A
    #define MODULE_A_EXPORT __declspec(dllexport)
#else
    #define MODULE_A_EXPORT __declspec(dllimport)
#endif

template class MODULE_A_EXPORT ModuleADeleter<Base>;
template class MODULE_A_EXPORT ModuleADeleter<Derived>;

// ModuleA/ModuleADeleter.cpp

#include "ModuleA/ModuleADeleter.h"

template <class T>
void ModuleADeleter<T>::operator()(T const *object) const {
    delete object;
}

template class ModuleADeleter<Base>;
template class ModuleADeleter<Derived>;

(Importing/exporting template instantiations from DLLs is described there).

At this point, we just have to return std::unique_ptr<Base, ModuleADeleter<Base>> from module A, and convert consistently to std::unique_ptr<Derived, ModuleADeleter<Derived>> as needed.

Note that ModuleADeleter<Derived> is only needed if Base has a non-virtual destructor, otherwise just reusing ModuleADeleter<Base> (as the linked answer would) will work as intended.


Method 2

The easiest solution is to used std::shared_ptr instead of std::unique_ptr. It has a bit of a performance penalty, but you don't need to implement and update a deleter, or convert it manually. This works because std::shared_ptr instantiates and type-erases its deleter upon construction, which is done inside module A. This deleter is then stored and kept until needed, and doesn't appear in the pointer's type, so you can mix pointers to objects instantiated from various modules freely.


Also... get_deleter returns a reference to an object lying in the old unique_ptr which is destroyed when this old unique_ptr is destroyed, is it not?

No, get_deleter's return value refers to the deleter contained in the unique_ptr on which you called it. The way the deleter's state is transferred when moving between unique_ptrs is described in overload #6 here.

Gooden answered 10/10, 2019 at 9:22 Comment(8)
Ahh, sorry for this additional question, clearly, we also move the deleter. But we do not have unique_ptr<Derived, default_delete<Derived>> here what would be what we need, right?Jenifferjenilee
@Jenifferjenilee it does, kind of. It does not have to be an actual function pointer: ModuleADeleter<Base> above, by its type alone, instructs the compiler and linker to route the call to its implementation, which resides in module A.Gooden
sorry for replacing my question to this nice answer, but yes, this makes senseJenifferjenilee
@Jenifferjenilee edit wars... ;) -- Did I answer your question, though? std::default_delete doesn't work because, since it is a template, every module will end up with its own instance of it, each calling its own module's heap. ModuleADeleter, on the other hand, cannot be instanciated by outsiders, so the call has to come back to module A (or fail to compile/link).Gooden
this all makes sense! So, we can conclude that there cannot be a multi-heap-space-safe version of static_unique_ptr_cast with result type unique_ptr<Derived> (without specializing 2nd template argument), good to know! Maybe one last question to be clear: Casting with your solution would then work by just using unique_ptr<Derived, ModuleADeleter<Derived>>(static_cast<Derived*>(oldUniquePtr.release()), oldUniquePtr.get_deleter())? I mean, second argument is a different type, so does an implicit cast works?Jenifferjenilee
@Jenifferjenilee you don't need the second argument, since the deleter instance is stateless :)Gooden
@Jenifferjenilee no, it just covers the general case and implicitly uses (and transfers) the same deleter, whether it is stateful or not. Note that the whole deleter replacement dance is only useful if Base has a non-virtual destructor, in which case the existence of polymorphic instances of std::unique_ptr<Base> is a bit worrisome in the first place. Otherwise, that answer works fine, provided that the deleter carries along the information about its origin, either in type or in value.Gooden
I agree with your design arguments. This answers my questions, thank you a lot for the effort and time!Jenifferjenilee

© 2022 - 2024 — McMap. All rights reserved.