Search code examples
c++templatescastingderived-class

How is it possible that std::shared_ptr<Derived> casts to std::shared_ptr<Base> with no compiler errors?


I'm trying to implement a custom smart pointer. So I have something like this:

// Base class for every object
class Base {
public:
    int n_holders {};
};

class Derived : public Base {};

// Custom shared pointer
template<class T>
class Sptr {
public:
    T* obj;
    Sptr(T* obj_) : obj{obj_} {}
};

void SomeFunc(Sptr<Base> obj) {}

void SomeFunc2(std::shared_ptr<Base> obj) {}

int main()
{
    auto a = Sptr<Base>(new Base());
    SomeFunc(a); // OK

    auto b = Sptr<Derived>(new Derived());
    SomeFunc(b); // Error. No matching function call to SomeFunc

    auto c = std::shared_ptr<Base>(new Base());
    SomeFunc2(c); // OK

    auto d = std::shared_ptr<Derived>(new Derived());
    SomeFunc2(d); // OK !!!
}

My question is why compiler can't automatically cast from Sptr<Derived> to Sptr<Base> if using std::shared_ptr there is no errors?. How to make it possible?


Solution

  • std::shared_ptr has a constructor (from cppreference):

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

    This overload ...

    Constructs a shared_ptr which shares ownership of the object managed by r. If r manages no object, this manages no object too. 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*.

    Hence, in some sense the tricky part is not to convert the shared pointer, but to prevent it when the pointer types are not implicitly convertible. You can use SFINAE to achieve that.

    Here is a toy example that has conversions from Bar<T1> to Bar<T2> (but not the other way around) enabled only when T1 inherits from T2:

    #include <type_traits>
    
    template <typename T1>
    struct Bar {
        Bar() {}
    
        template <typename T2, typename std::enable_if_t<std::is_base_of_v<T1,T2>,int> = 0>
        Bar(Bar<T2>){} 
    
    };
    
    struct Foo {};
    struct Derived : Foo {};
    
    int main(){
        Bar<Derived> d;
        Bar<Foo> b;
        //d = b; // eror
        b = d;  // OK
    }
    

    Live Demo

    You probably want it more general, like shared pointer, to allow such conversion whenever a T2* can be converted to a T1*, not only when they inherit from each other (see std::is_convertible, I have to admit, I don't really understand the change that came with C++17, so I can only guess: maybe its std::is_layout_compatible in that case). So to mimic a pre-C++17 smart pointer you could use:

        template <typename T2, typename std::enable_if_t<std::is_convertible_v<T2*,T1*>,int> = 0>
        Bar(Bar<T2>){} 
    

    which enables the conversion for all T2 where T2* can be converted to a T1*.