Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do `shared_ptr`s achieve covariance?

It is possible to copy or construct a shared_ptr<Base> from shared_ptr<Deriver> (i.e. shared_ptr<Base> ptr = make_shared<Derived>()). But as we all know, template classes are not convertible to each other, even if the template arguments are. So how can shared_ptrs check if the value of their pointers are convertible and do the conversion if they are?

like image 921
GamefanA Avatar asked Oct 16 '22 05:10

GamefanA


1 Answers

Yes, specializations of the same class template by default have almost no relationship and are essentially treated like unrelated types. But you can always define implicit conversions between class types by defining converting constructors (To::To(const From&)) and/or conversion functions (From::operator To() const).

So what std::shared_ptr does is define template converting constructors:

namespace std {
    template <class T>
    class shared_ptr {
    public:
        template <class Y>
        shared_ptr(const shared_ptr<Y>&);
        template <class Y>
        shared_ptr(shared_ptr<Y>&&);
        // ...
    };
}

Though the declaration as shown would allow conversions from any shared_ptr to any other, not just when the template argument types are compatible. But the Standard also says about these constructors ([util.smartptr]/5 and [util.smartptr.const]/18 and util.smartptr.const]/21):

For the purposes of subclause [util.smartptr], a pointer type Y* is said to be compatible with a pointer type T* when either Y* is convertible to T* or Y is U[N] and T is cv U[].

The [...] constructor shall not participate in overload resolution unless Y* is compatible with T*.

Although this restriction could be done in any way, including compiler-specific features, most implementations will enforce the restriction using an SFINAE technique (Substitution Failure Is Not An Error). One possible implementation:

#include <cstddef>
#include <type_traits>

namespace std {
    template <class Y, class T>
    struct __smartptr_compatible
        : is_convertible<Y*, T*> {};

    template <class U, class V, size_t N>
    struct __smartptr_compatible<U[N], V[]>
        : bool_constant<is_same_v<remove_cv_t<U>, remove_cv_t<V>> &&
                        is_convertible_v<U*, V*>> {};

    template <class T>
    class shared_ptr {
    public:
        template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
        shared_ptr(const shared_ptr<Y>&);

        template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
        shared_ptr(shared_ptr<Y>&&);

        // ...
    };
}

Here the helper template __smartptr_compatible<Y, T> acts as a "trait": it has a static constexpr member value which is true when the types are compatible as defined, or false otherwise. Then std::enable_if is a trait which has a member type called type when its first template argument is true, or does not have a member named type when its first template argument is false, making the type alias std::enable_if_t invalid.

So if template type deduction for either constructor deduces the type Y so that Y* is not compatible with T*, substituting that Y into the enable_if_t default template argument is invalid. Since that happens while substituting a deduced template argument, the effect is just to remove the entire function template from consideration for overload resolution. Sometimes an SFINAE technique is used to force selecting a different overload instead, or as here (most of the time), it can just make the user's code fail to compile. Though in the case of the compile error, it will help that a message appears somewhere in the output that the template was invalid, rather than some error even deeper within internal template code. (Also, an SFINAE setup like this makes it possible for a different template to use its own SFINAE technique to test whether or not a certain template specialization, type-dependent expression, etc. is or isn't valid.)

like image 107
aschepler Avatar answered Oct 31 '22 20:10

aschepler