Why can't shared_ptr resolve inheritance relat

2019-03-31 20:36发布

问题:

Here is a simplified example:

#include <memory>
#include <vector>

template< class T >
class K 
{
public:
    virtual ~K(){}
};

class KBOUM : public K<int>{};

template< class U >
void do_something( std::shared_ptr< K<U> > k ) { }

int main()
{
   auto kboom = std::make_shared<KBOUM>();
    do_something( kboom );  // 1 : error

   std::shared_ptr< K<int> > k = kboom; // 2 : ok
   do_something( k ); // 3 : ok

}

With or without boost, whatever the compiler I use I get an error on #1 because shared_ptr<KBOOM> don't inherit from shared_ptr<K<int>> . However, KBOOM does inherit from K<int>. You can see that #2 works because shared_ptr is designed to allow implicitly passing a child class pointer to a base class pointer, like raw pointers.

So my questions are:

  1. what prevent std::shared_ptr implementer to make it work in case #1 (I mean, assuming that the standard does prevent this case, there should be a reason);
  2. is there a way to write auto kboom = std::make_shared<KBOUM>(); do_something( kboom ); without looking the int type from K from which KBOOM inherit?

Note: I want to avoid the user of the function to have to write

std::shared_ptr<K<int>> k = std::make_shared<KBOOM>();

or

do_something( std::shared_ptr<K<int>>( kboom ) );

回答1:

This is not related to std::shared_ptr<>. In fact, you could replace that with any class template and get the very same result:

template<typename T> struct X { };

class KBOUM : public X<int> { };

template<typename U>
void do_something(X<K<U>> k) { }

int main()
{
    X<KBOUM> kboom;
    do_something(kboom); // ERROR!

    X<K<int>> k;
    do_something(k); // OK
}

The problem here is that type argument deduction is trying to find a perfect match, and derived-to-base conversions are not attempted.

Only after all template parameters have been unambiguously deduce to produce a perfect match (with the few exceptions allowed by the Standard), possible conversions between arguments are considered during overload resolution.

WORKAROUND:

It is possible to figure out a workaround based on a solution posted by KerrekSB in this Q&A on StackOverflow. First of all, we should define a type trait that allow us to tell whether a certain class is derived from an instance of a certain template:

#include <type_traits>

template <typename T, template <typename> class Tmpl>
struct is_derived
{
    typedef char yes[1];
    typedef char no[2];

    static no & test(...);

    template <typename U>
    static yes & test(Tmpl<U> const &);

    static bool const value = sizeof(test(std::declval<T>())) == sizeof(yes);
};

Then, we could use SFINAE to rewrite do_something() as follows (notice that C++11 allows default arguments for function template parameters):

template<class T, std::enable_if<is_derived<T, K>::value>* = nullptr>
void do_something(X<T> k) 
{ 
    // ...
}

With these changes, the program will correctly compile:

int main()
{
    X<KBOUM> kboom;
    do_something(kboom); // OK

    X<K<int>> k;
    do_something(k); // OK
}

And here is a live example.



回答2:

Andy Prowl gave a perfect explanation about the issue and suggested a clever workaround. I guess with some effort the workaround can be adapted to C++03 as well. (I haven't tried that. This is just a guess.)

I only want to suggest a simpler workaround which can only work for C++11. All you need to do is create this overload:

template< class T >
auto do_something(const std::shared_ptr<T>& k ) ->
    decltype(do_something( std::shared_ptr< K<int> >( k )))
{
    return do_something( std::shared_ptr< K<int> >( k ));
}

Basically, it probes if do_something( std::shared_ptr< K<int> >( k )) is legal through a decltype (and SFINAE). If so, then this overload performs the "cast to base" and delegates the call to the overload that takes shared_ptr to the base class.

Update:

More generally, if you have a function, say do_something that accepts a shared_ptr<Base> and you want the compiler to call it when you pass a shared_ptr<T> where T is any type that publicly derives from Base then the workaround is this:

class Base {};
class Derived : public Base {};

// The original function that takes a std::shared_ptr<Base>
void do_something( const std::shared_ptr<Base>& ) {
    // ...
}

// The workaround to take a shared_ptr<T> where T publicly derives from Base
template <typename T>
auto do_something(const std::shared_ptr<T>& pd) ->
    decltype( do_something( std::shared_ptr<Base>( pd ) ) ) {
   return do_something( std::shared_ptr<Base>( pd ) );
}

// Example:
int main() {
    auto pd = std::make_shared<Derived>();
    do_something( pd );   
}


回答3:

#include <memory>
#include <utility>

template< template<typename T>class Factory, typename T >
struct invert_factory {};

template< template<typename T>class Factory, typename U >
struct invert_factory< Factory, Factory<U> > {
  typedef U type;
};
template<typename T>
struct K {};

template<template<typename>class Factory, typename Default, typename U>
U invert_implicit_function( Factory<U> const& );
template<template<typename>class Factory, typename Default>
Default invert_implicit_function( ... );

template<template<typename>class Factory, typename U>
struct invert_implicit {
private:
   struct unused_type{};
public:
   typedef decltype( invert_implicit_function<Factory, unused_type>( std::declval<U>() ) ) type;
   enum{ value = !std::is_same< unused_type, type >::value };
};

template<typename spKU, typename=void >
struct is_shared_ptr_to_KU {};

template<typename spKU>
struct is_shared_ptr_to_KU< spKU,
  typename std::enable_if<
     invert_implicit< K,
          typename invert_factory<std::shared_ptr, spKU>::type
     >::value
  >::type
>:std::true_type {};

template< typename spKU >
auto do_something( spKU )->typename std::enable_if< is_shared_ptr_to_KU<spKU>::value >::type { }

struct Blah:K<int> {};
int main() {
  static_assert(invert_implicit< K, K<int> >::value, "one");
  static_assert(invert_implicit< K, Blah >::value, "two");
  do_something( std::shared_ptr<K<int>>() );
  do_something( std::shared_ptr<Blah>() );
 // do_something( 0 );
 // do_something( std::shared_ptr<int>() );
}

needs a bit of polish, but does what you are asking.

Handleing shared_ptr<K<U>> took an extra level of indirection.

Also included is ways to actually extract the U type if needed. (if invert_implicit::value is true, then invert_implicit::type is U).

Note that classes that can be implicitly cast to K<U> qualify -- checking is_derived can also be done if you prefer.