Search code examples
c++castingshared-ptr

How const std::shared_ptr reference upcast works?


According to my understanding, when a const shared_ptr& is upcasted, it creates a new shared pointer of the based class pointing to the same (casted) object.

#include <iostream>
#include <memory>

class Animal
{
};

class Cat : public Animal
{
};

void display(const std::shared_ptr<Animal>& animal)
{
    std::cout << animal.use_count() << std::endl;
}

int main()
{
    auto cat = std::make_shared<Cat>();

    std::cout << cat.use_count() << std::endl;
    display(cat);
    std::cout << cat.use_count() << std::endl;
}

The output of the above code is as follow.

sujith@AKLJoincggDLEd:~/sujith$ g++  -std=c++17 main.cpp
sujith@AKLJoincggDLEd:~/sujith$ ./a.out
1
2
1
sujith@AKLJoincggDLEd:~/sujith$

My question is what operator does this? Can we get the same behaviour for other reference upcasting as well? Or is it speficially handled for shared_ptr references?

Thank you for looking into this.


Solution

  • The function that does this is std::shared_ptr's constructor, specifically overload 9:

    template< class Y >
    shared_ptr( const shared_ptr<Y>& r ) noexcept;
    

    These are the semantics:

    Constructs a shared_ptr which shares ownership of the object managed by r. If r manages no object, *this manages no object either. The template overload doesn't participate in overload resolution if Y* is not implicitly convertible to (until C++17)compatible with (since C++17) T*.

    Important is the last sentence. Your Cat* (Y*) is convertible to / compatible with your Animal* (T*). So, this constructor participates in overload resolution and is selected because it is the only one available (the other overloads do not match).


    Here is a simplified example that highlights how the general concept works, that std::shared_ptr uses in the quoted overload:

    struct A{};
    
    struct B {
      B(A const& a) {}
    };
    
    void foo(B const&) {}
    
    int main() {
      A a;
      foo(a);
    }
    

    The call to foo implicitly creates a B object via B::B(A const&). foo is then passed a const reference to that temporary B object.

    The fact that in your example B and A were types stemming from the same class template is immaterial.