"Downcasting" unique_ptr<Base> to unique_ptr<Derived>
Asked Answered
S

1

72

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.

Skindive answered 16/1, 2014 at 22:53 Comment(7)
I would let CastToDerived take a unique_ptr<T>&&. The reason why there are no casts equivalent to static_pointer_cast for shared_ptr is that casts typically do not modify their argument. But for unique_ptr, you'd have to move the pointer from the argument to the object returned by the cast.Vookles
@Vookles I assume that swap could be a good option too in this caseSchaffel
@Schaffel Could you elaborate?Vookles
@Skindive (continuing..) Your CastToDerived could be called via CastToDerived(my_ptr.get()) (which is an error) and CastToDerived(my_ptr.release()) (which is correct). To prevent the former, I suggest using something like CastToDerived( std::move(my_ptr) ) which is explicit and maybe a bit less error prone. Alternatively, make it explicit in the name, like move_static_cast<Derived>(my_ptr).Vookles
So unique_ptr<T>&& (or rather unique_ptr<Derived>&&, as templated types cannot be exposed across DLL boundaries) would implicitly perform the move / transfer? Could I then just do such a move operation directly without the need for a function? And would such a move retain the deleter from the original unique_ptr?Skindive
Yes, the way it is sketched implies that the caller does a my_ptr.release(). The reason for that is of course that the caller must then be aware of the release.Skindive
@Skindive Yes, but IMHO that's error prone, as the caller might as well use my_ptr.get() instead. Therefore either enforce the use of a std::move at the call site or let the cast function move the pointer, but then the name of the cast function needs to convey that it alters its argument.Vookles
C
50

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.

Count answered 16/1, 2014 at 23:23 Comment(30)
You need to extract the deleter from the source object and inject it into the destination object. The type is most probably not sufficient.Variscite
@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()?Count
@Count [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 releaseVookles
@Count should it be ...result != dynamic_cast<... ?Skindive
@Skindive 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.Count
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.Skindive
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.Vookles
@Vookles 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.Count
Sorry for all the trouble ;)Vookles
@Vookles Not any trouble at all, constructive criticism is always welcome.Count
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)Vookles
@Vookles 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.Count
@Count 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 :)Mascon
@Mascon 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.Count
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?Cattima
@Cattima 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.Count
Shouldn't the function take the argument unique_ptr by value, because it will take ownership of p? See the discussion here: #21175093Refulgent
@Refulgent 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.Count
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 :-(.Refulgent
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?Mohawk
@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.Count
@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.Mohawk
To fix the throw problem, first create retval temporary. Then release source pointer. Then return temporary.Mohawk
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> ?Toothbrush
I think I might have fixed it with some enable_if: rextester.com/KFSF97862Toothbrush
@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?Ochrea
Ok, since no one answers here anymore, maybe consider this follow up question which clarifies a bit more: #58319216Ochrea
@Ochrea 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.Toothbrush
@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.Ochrea
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.Ushaushant

© 2022 - 2024 — McMap. All rights reserved.