6
\$\begingroup\$

dynamic_pointer_cast is only implemented for std::shared_ptr. I need the same functionality for unique pointers.

The wrinkle is that dynamic_casting a pointer could fail (yield nullptr), so what do we want to happen then? I decided that in that case I would like the original pointer to remain unchanged.

I have implemented the following:

template< typename T, typename S >
inline std::unique_ptr<T> dynamic_pointer_cast(std::unique_ptr<S>&& ptr_)
{
    T* const converted_ptr = dynamic_cast<T*>(ptr_.get());
    if (!converted_ptr)
        // cast failed, leave input untouched, return nullptr
        return nullptr;

    // cast succeeded, clear input, return casted ptr
    ptr_.release();
    return std::unique_ptr<T>(converted_ptr);
}

Testing code:

#include <memory>
int main(int argc, char **argv)
{
    std::unique_ptr<Base> basePtr = std::make_unique<Derived1>();
    // this should fail, basePtr should remain non-empty, return should be empty
    auto deriv2Ptr = dynamic_pointer_cast<Derived2>(std::move(basePtr));
    // this should succeed, basePtr should become empty, return should be non-empty
    auto deriv1Ptr = dynamic_pointer_cast<Derived1>(std::move(basePtr));
    
    return 0;
}

Is this safe? Does the interface make sense? For the latter question: I decided to take an R-value ref so users have to write std::move, denoting pointer will be emptied. But then it may not if the cast fails... Normal ref is the other option, but then it's less clear at the call site that the unique_ptr will likely be cleared.

\$\endgroup\$
0

3 Answers 3

4
\$\begingroup\$

The dynamic-cast function

I'm pretty sure <memory> needs to be included before the template function's definition (GCC certainly thinks so).

Passing as an rvalue-reference seems exactly the right choice to me - it shows that the function is at least potentially a sink. Obviously if an actual rvalue is bound to that reference, and the cast fails, the pointed-to object is destroyed. But I don't think that will surprise anyone.

I believe it can safely be declared noexcept.

I would probably write the return nullptr as return {} to emphasise that the return type isn't a raw pointer.

We could turn the condition around so that we test the positive case; that allows us to declare and initialise converted_ptr in the condition:

template<typename T, typename S>
std::unique_ptr<T> dynamic_pointer_cast(std::unique_ptr<S>&& p) noexcept
{
    if (T* const converted = dynamic_cast<T*>(p.get())) {
        // cast succeeded; clear input
        p.release();
        return std::unique_ptr<T>{converted};
    }

    // cast failed; leave input untouched
    return {};
}

Or, always create a (possibly-null) unique pointer, and use its bool conversion as test:

template<typename T, typename S>
std::unique_ptr<T> dynamic_pointer_cast(std::unique_ptr<S>&& p) noexcept
{
    auto converted = std::unique_ptr<T>{dynamic_cast<T*>(p.get())};
    if (converted) {
        p.release();            // no longer owns the pointer
    }
    return converted;
}

I think this last version is the simplest and clearest.

As another answer suggested, consider moving (not copying, if we want noexcept) the deleter into the new unique-pointer. We can use get_deleter() to access it by reference; it probably makes sense to swap the new and old pointers' deleters:

#include <memory>
template<typename T, typename S, typename Deleter>
auto dynamic_pointer_cast(std::unique_ptr<S, Deleter>&& p) noexcept
{
    auto converted = std::unique_ptr<T, Deleter>{dynamic_cast<T*>(p.get())};
    if (converted) {
        std::swap(converted.get_deleter(), p.get_deleter());
        p.release();            // no longer owns the pointer
    }
    return converted;
}

The test program

This wouldn't compile, due to the lack of definitions for Base, Derived1 and Derived2. I took a guess at:

struct Base{ virtual ~Base()=default; };
struct Derived1 : Base{};
struct Derived2 : Base{};

The test program doesn't use its command-line arguments, so prefer int main() instead of int main(int argc, char **argv).

It always returns 0 (success), rather than actually checking the expectations written in the comments. I fixed that before refactoring the function, to give more confidence:

int main()
{
    std::unique_ptr<Base> b = std::make_unique<Derived1>();
    // this should fail: b should remain non-empty, return should be empty
    auto d2 = dynamic_pointer_cast<Derived2>(std::move(b));
    // this should succeed: b should become empty, return should be non-empty
    auto d1 = dynamic_pointer_cast<Derived1>(std::move(b));
    return d2 || !d1 || b;
}
\$\endgroup\$
3
\$\begingroup\$
  • Since it's a template function, it doesn't need to be marked inline.

  • It would be nice to handle custom deleters too. We can access the deleter with ptr_.get_deleter().

\$\endgroup\$
2
\$\begingroup\$

actually, it's better if you did not modify the deleter, since they are different types. that means in some condition which uses default deleter, it won't compile:


struct base {
  int* a{new int(1)};
  virtual ~base() {
    delete a;
  }
};

struct derived : base {
  int* b{new int(2)};
  ~derived() {
    delete b;
  }
};

void func(std::unique_ptr<derived>&& d) {
  d = nullptr;
}

int main() {
  std::unique_ptr<base> b = std::make_unique<derived>();
  auto d = dynamic_pointer_cast<derived>(std::move(b));
  func(std::move(d));
  return 0;
}

if the dynamic_pointer_cast keeps the deleter of base, it would not match the func here which requires default deleter of derived. the custom deleter is in rare case. it's better to care about default conditions

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.