Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is static downcasting unique_ptr unsafe?

Tags:

c++

unique-ptr

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)

like image 502
IceFire Avatar asked Oct 10 '19 08:10

IceFire


1 Answers

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.

like image 198
Quentin Avatar answered Nov 10 '22 20:11

Quentin