"Downcasting" unique_ptr<Base> to unique_ptr<Derived>

27,376

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.

Share:
27,376
d7samurai
Author by

d7samurai

Badges? I don't need no stinkin' badges!

Updated on July 05, 2022

Comments

  • d7samurai
    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.e unique_ptr<Derived>, unique_ptr<DerivedA>, unique_ptr<DerivedB>etc.

    Given DerivedA : Derived and Derived : 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 another unique_ptr of the desired flavor (the release 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
    David Rodríguez - dribeas over 10 years
    You need to extract the deleter from the source object and inject it into the destination object. The type is most probably not sufficient.
  • Praetorian
    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 called release()?
  • dyp
    dyp over 10 years
    @Praetorian [unique.ptr.single.asgn] for example says: "Effects: Transfers ownership from u to *this as if by calling reset(u.release()) followed by an assignment from std::forward<D>(u.get_deleter())." So I think it's well-defined, although the deleter doesn't occur in the postconditions of release
  • d7samurai
    d7samurai over 10 years
    @Praetorian should it be ...result != dynamic_cast<... ?
  • Praetorian
    Praetorian over 10 years
    @d7samurai No, I'm assigning the result of the dynamic_cast to result and checking whether it is nullptr, all in one expression. But now it seems unnecessary to do that. I'll update the answer.
  • d7samurai
    d7samurai over 10 years
    Indeed 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
    dyp over 10 years
    Btw. 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
    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 the dynamic_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 the move to the last one.
  • dyp
    dyp over 10 years
    Ok, I see. Nice idea.
  • dyp
    dyp over 10 years
    Sorry for all the trouble ;)
  • Praetorian
    Praetorian over 10 years
    @dyp Not any trouble at all, constructive criticism is always welcome.
  • dyp
    dyp over 10 years
    Why 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. The copy could be tricky, though. (sry for hijacking this question)
  • Praetorian
    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
    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
    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 type unique_ptr<Derived, default_delete<Base>>, and you're trying to assign it to a unique_ptr<Derived, default_delete<Derived>>. Just use auto d = dynamic_unique_ptr_cast<Derived, Base>(std::move(b)); instead. And ~Base must be virtual for the second example to work.
  • Malvineous
    Malvineous almost 9 years
    What happens if after p.release(), in the unique_ptr constructor, an exception is thrown? Will the pointer be leaked? Can you call p.release() after the unique_ptr constructor (and before return) to avoid this? Since you are moving the deleter during the constructor you should never end up with two unique_ptr objects deleting the same thing, right?
  • Praetorian
    Praetorian almost 9 years
    @Malvineous unique_ptr constructors are noexcept. But what you suggest, first constructing the second unique_ptr, and then release()ing ownership of the original, would be the more foolproof option if copying/moving the deleter can throw.
  • Knitschi
    Knitschi over 8 years
    Shouldn'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
    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
    Knitschi over 8 years
    Yes 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
    Yakk - Adam Nevraumont over 7 years
    If 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
    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 whether default_delete is sufficient, but that would solve the throwing move problem in some cases. About double deletion, are you asking about the return statement where nullptr is returned? The deleter should never be called for the returned unique_ptr in that case.
  • Yakk - Adam Nevraumont
    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
    Yakk - Adam Nevraumont over 7 years
    To fix the throw problem, first create retval temporary. Then release source pointer. Then return temporary.
  • Jean-Simon Brochu
    Jean-Simon Brochu over 6 years
    I 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
    Jean-Simon Brochu over 6 years
    I think I might have fixed it with some enable_if: rextester.com/KFSF97862
  • IceFire
    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 expensive dynamic_cast, which is unnecessary. I am also looking for a solution, this answer needs a unique_ptr as return type whose first template argument fits Derived but we are looking for unique_ptr<Derived>, not anything else. Or is there no solution?
  • IceFire
    IceFire over 4 years
    Ok, since no one answers here anymore, maybe consider this follow up question which clarifies a bit more: stackoverflow.com/questions/58318716/…
  • Jean-Simon Brochu
    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
    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
    Kuba hasn't forgotten Monica almost 4 years
    This 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.