Why does ReferenceCountedObjectPtr allow an implicit downcast?

I was expecting ReferenceCountedObjectPtr to work like std::shared_ptr, in that it would implicitly upcast, but fail to compile if I attempted an implicit downcast.

However, ReferenceCountedObjectPtr will happily downcast with no warnings, resulting in undefined behaviour. It appears to be performing a simple static_cast without any constraints. Is this intended? If so why?

class Base : public ReferenceCountedObject
{
};

class Derived : public Base
{
};

int main (int argc, char* argv[])
{
    // upcast ReferenceCountedObjectPtr
    ReferenceCountedObjectPtr<Derived> d1(new Derived());
    ReferenceCountedObjectPtr<Base> b1 = d1; // upcast is ok

    // downcast ReferenceCountedObjectPtr
    ReferenceCountedObjectPtr<Base> b2(new Base());
    ReferenceCountedObjectPtr<Derived> d2 = b2; // expect downcast to fail, but it compiles, and has undefined behaviour
    
    // upcast shared_ptr
    auto d3 = std::make_shared<Derived>();
    std::shared_ptr<Base> b3 = d3; // upcast is ok

    // downcast shared_ptr fails
    auto b4 = std::make_shared<Base>();
    std::shared_ptr<Derived> d4 = b4; // downcast fails as expected, with "no viable conversion ..."
}

Hmm, well spotted! I’ll push a fix for this shortly!

Excellent, thanks - I’ll keep an eye out for it in BREAKING-CHANGES.txt!

will ReferenceCountedObject eventually be replaced by std::shared_ptr in the framework? Kind of like how ScopedPointer was replaced with std::unique_ptr…

Maybe one day. But it’s not as simple.

Unlike shared_ptr, ReferenceCountedObjectPtr is an intrusive ref-count, so you can convert one to a raw pointer and back again without things going wrong, which isn’t true of shared_ptr. So if we just search-replaced it in the codebase, we’d introduce all kinds of subtle, hidden bugs.

Performance is also not straightforward. ReferenceCountedObjectPtr can be used for either atomic or non-atomic classes, which isn’t true of shared_ptr (AFAIK… though maybe there are ways to do that?), and its performance will vary between platforms.

So basically, sure, in many places it could be swapped, but each one would take careful checking and consideration, which isn’t work we’re planning on doing soon.

for shared_ptr atomic operations, you gotta use std::atomic_store() and std::atomic_load() to atomically swap what the shared_ptr points to.

There is more to do to be thread safe, if I understand that correctly:

  • decrement reference count for original pointee
  • update the pointee
  • increment the reference count for new pointee

All as atomic operation. That is not trivial to wrap into a atomic macro I assume.

well, thinking again, it might still be safe, if you update the pointee last…

check out Timur’s talk on the subject, he shows how to share objects between threads atomically and in a thread-safe manner using shared_ptr.

is there any way to do a dynamic_cast of a ReferenceCountedObjectPtr or is this illegal for some reason?

You should be able to cast the raw pointer (using .get()) and wrap it in another ReferenceCountedObjectPtr. I don’t see why this would cause problems, though of course the result might be null.

That’s the answer, with one clarification:
You can only wrap a ReferenceCountedObject into a ReferenceCountedObjectPtr, because it uses the intrusive reference count.
If you want to wrap an arbitrary object into a reference counted pointer, you need to use a non-intrusive pointer like std::shared_ptr. Here is the reference counter part of the shared_ptr.

TL;DR: You can cast the raw pointer you get returned with .get() to anything it inherits from or even down cast using dynamic_cast. But the Ptr is just the template, not the pointee.