"Downcasting" unique_ptr<Base> to unique_ptr<Derived>
I'd create a couple of function templates, static_unique_ptr_cast
and dynamic_unique_ptr_cast
. Use the former in cases where you're absolutely certain the pointer is actually a Derived *
, otherwise use the latter.
template<typename Derived, typename Base, typename Del>
std::unique_ptr<Derived, Del>
static_unique_ptr_cast( std::unique_ptr<Base, Del>&& p )
{
auto d = static_cast<Derived *>(p.release());
return std::unique_ptr<Derived, Del>(d, std::move(p.get_deleter()));
}
template<typename Derived, typename Base, typename Del>
std::unique_ptr<Derived, Del>
dynamic_unique_ptr_cast( std::unique_ptr<Base, Del>&& p )
{
if(Derived *result = dynamic_cast<Derived *>(p.get())) {
p.release();
return std::unique_ptr<Derived, Del>(result, std::move(p.get_deleter()));
}
return std::unique_ptr<Derived, Del>(nullptr, p.get_deleter());
}
The functions are taking an rvalue reference to ensure that you're not pulling the rug out from underneath the caller's feet by stealing the unique_ptr
passed to you.
Comments
-
d7samurai almost 2 years
I have a series of factories that return
unique_ptr<Base>
. Under the hood, though, they are providing pointers to various derived types, i.eunique_ptr<Derived>
,unique_ptr<DerivedA>
,unique_ptr<DerivedB>
etc.Given
DerivedA : Derived
andDerived : Base
we'd have:unique_ptr<Base> DerivedAFactory() { return unique_ptr<Base>(new DerivedA); }
What I need to do is to "cast" the pointer from the returned
unique_ptr<Base>
to some derived level (not necessarily the original internal one). To illustrate in pseudo code:unique_ptr<Derived> ptr = static_cast<unique_ptr<Derived>>(DerivedAFactory());
I'm thinking of doing this by releasing the object from the
unique_ptr
, then using a function that casts the raw pointer and reassigns that to anotherunique_ptr
of the desired flavor (therelease
would be explicitly done by the caller prior to the call):unique_ptr<Derived> CastToDerived(Base* obj) { return unique_ptr<Derived>(static_cast<Derived*>(obj)); }
Is this valid, or is / will there be something funky going on?
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.
-
David Rodríguez - dribeas over 10 yearsYou need to extract the deleter from the source object and inject it into the destination object. The type is most probably not sufficient.
-
Praetorian over 10 years@DavidRodríguez-dribeas Good point, thanks. I've updated the answer. Any idea whether
get_deleter()
must return a valid result after you've calledrelease()
? -
dyp over 10 years@Praetorian [unique.ptr.single.asgn] for example says: "Effects: Transfers ownership from
u
to*this
as if by callingreset(u.release())
followed by an assignment fromstd::forward<D>(u.get_deleter())
." So I think it's well-defined, although the deleter doesn't occur in the postconditions ofrelease
-
d7samurai over 10 years@Praetorian should it be
...result != dynamic_cast<...
? -
Praetorian over 10 years@d7samurai No, I'm assigning the result of the
dynamic_cast
toresult
and checking whether it isnullptr
, all in one expression. But now it seems unnecessary to do that. I'll update the answer. -
d7samurai over 10 yearsIndeed you are :) I wasn't looking closely enough. My own implementation of the same check (the one that enables me to subsequently use static_cast) compares the result of the dynamic_cast against a nullptr, since the compiler said something about implicit conversion to bool).. I was seeing my code instead of yours, hence the misread.
-
dyp over 10 yearsBtw. the deleter can be move-only, this makes the final return statement a bit difficult (you can't copy, you shouldn't move either, and maybe you can't default-construct). Maybe if it's move-only, you could require
DefaultConstructible
; another way is to throw an exception instead of returning. -
Praetorian over 10 years@dyp Thanks, you just made me realize I could've been moving the deleter all along. Maybe you could SFINAE using
is_move_constructible
to a version that throws when thedynamic_cast
fails. But I think I prefer the compilation failure in this case to silently switching between versions that throw and do not throw. Edit: I didn't mean add themove
to the last one. -
dyp over 10 yearsOk, I see. Nice idea.
-
dyp over 10 yearsSorry for all the trouble ;)
-
Praetorian over 10 years@dyp Not any trouble at all, constructive criticism is always welcome.
-
dyp over 10 yearsWhy did you delete that answer? All I wanted to note is that you can't just use any random StdLib's
set_difference
without paying attention if it provides the additional guarantee to work on overlapping ranges. Thecopy
could be tricky, though. (sry for hijacking this question) -
Praetorian over 10 years@dyp Because he is trying to do it in-place. And the one I posted doesn't quite cut it in that case. It contains potential self-assignments, which would invalidate iterators (I think) and a call to
std::copy
which again requires non-overlapping ranges. -
georgemp almost 10 years@Praetorian Hi, I've been trying to use this and am able to get it to work when I use a custom deleter while initializing the unique_ptr(pastebin.com/EABRT9G3). But, am not sure of how to do it with the default deleter (pastebin.com/BNDBivff) - In this case, the compiler complains about there not being a viable conversion between the derived/base class deleters. Is it possible to use this code with default delteters? If so, could you look at my second paste and let me know how I need to call the cast function with the right template arguments? Thanks :)
-
Praetorian almost 10 years@georgemp That won't work because both the
unique_ptr
cast functions above make a copy of the deleter as is, so in your last example, the return value from the function is of typeunique_ptr<Derived, default_delete<Base>>
, and you're trying to assign it to aunique_ptr<Derived, default_delete<Derived>>
. Just useauto d = dynamic_unique_ptr_cast<Derived, Base>(std::move(b));
instead. And~Base
must bevirtual
for the second example to work. -
Malvineous almost 9 yearsWhat happens if after
p.release()
, in theunique_ptr
constructor, an exception is thrown? Will the pointer be leaked? Can you callp.release()
after theunique_ptr
constructor (and beforereturn
) to avoid this? Since you are moving the deleter during the constructor you should never end up with twounique_ptr
objects deleting the same thing, right? -
Praetorian almost 9 years@Malvineous
unique_ptr
constructors arenoexcept
. But what you suggest, first constructing the secondunique_ptr
, and thenrelease()
ing ownership of the original, would be the more foolproof option if copying/moving the deleter can throw. -
Knitschi over 8 yearsShouldn't the function take the argument unique_ptr by value, because it will take ownership of p? See the discussion here: stackoverflow.com/questions/21174593/…
-
Praetorian over 8 years@Knitschi Maybe you meant to link to this answer? If yes, I agree, taking the argument by value isn't a bad idea either. But there is some debate on the subject, for example Scott Meyers argues it should be taken by rvalue reference here.
-
Knitschi over 8 yearsYes I wanted to link to that answer. Thanks for informing me that this topic is still under debate. Now I don't know how to write c++ anymore :-(.
-
Yakk - Adam Nevraumont over 7 yearsIf moving the deleter throws the code above leaks? Second, is there any way to detect if both the default deleter is being used and it is sufficient? (Ie virtual dtor) Also dynamic version double-deletes?
-
Praetorian over 7 years@Yakk Yes, if moving the deleter throws there's a leak, I don't see a way around that, do you? I suppose you could tag dispatch to handle
default_delete
being used, but is it worth it since copying/moving that is cheap? I don't know how you determine whetherdefault_delete
is sufficient, but that would solve the throwing move problem in some cases. About double deletion, are you asking about the return statement wherenullptr
is returned? The deleter should never be called for the returnedunique_ptr
in that case. -
Yakk - Adam Nevraumont over 7 years@praet no, I just somehow missed the
p.release();
line. In that case you do end up eith a moved-from unique ptr that did not end up empty, which may surprise. I do not know how to avoid that. -
Yakk - Adam Nevraumont over 7 yearsTo fix the throw problem, first create retval temporary. Then release source pointer. Then return temporary.
-
Jean-Simon Brochu over 6 yearsI don't know which compiler you are using but without using the auto keyword, this cannot compile if you are trying to put the result of the static/dynamic cast in a unique_ptr<Derived> as the op stated. rextester.com/QNOP12810. The link I provided shows which type you would have to carry around for this static cast to work. This is less that ideal. Any idea how to make it work with a real unique_ptr<Derived> ?
-
Jean-Simon Brochu over 6 yearsI think I might have fixed it with some enable_if: rextester.com/KFSF97862
-
IceFire over 4 years@Jean-SimonBrochu why should this fix anything? The deleter still has the same type and your example does not compile. The lean
static_cast
is just replaced by an expensivedynamic_cast
, which is unnecessary. I am also looking for a solution, this answer needs a unique_ptr as return type whose first template argument fitsDerived
but we are looking forunique_ptr<Derived>
, not anything else. Or is there no solution? -
IceFire over 4 yearsOk, since no one answers here anymore, maybe consider this follow up question which clarifies a bit more: stackoverflow.com/questions/58318716/…
-
Jean-Simon Brochu over 4 years@IceFire It does compile ( as you can verify by following the link I provided ) but it does not help with the dynamic_cast as you mentioned. I just tried to fix the code that was not compiling and tried to understand what the answer fixed... which I don't think is anything.
-
IceFire over 4 years@Jean-SimonBrochu yeah, it does now. I was looking at a time where the code did not... anyways, never mind. The answer to my follow up question under the link helped me at least a bit, maybe it also is of value for you.
-
Kuba hasn't forgotten Monica almost 4 yearsThis one simply doesn't work when the deleters are different. Period. If someone claims otherwise, get a link to godbolt. I had no success whatsoever making this work for any non-trivial type.