matovitch
matovitch

Reputation: 1284

Template deduction failure of template of template (with inheritance in-between), is there a better way ?

The problem

I had the following scheme in my code,

#include <memory>
#include <iostream>

template <class T>
struct A {};

template <class T>
struct Z {};

template <class T, class U>
struct B : A<T>, Z<U> {};

template <class T>
struct C : B<T, T> {};

template <class T>
void foo(const std::shared_ptr<A<T>>& a)
{
    std::cout << "HI !" << std::endl;    
}

int main()
{    
    auto c = std::make_shared<C<char>>();

    foo(c);

    return 0; 
}

But the compiler could not substitute the template parameters properly:

main.cpp: In function 'int main()':
main.cpp:26:10: error: no matching function for call to 'foo(std::shared_ptr<C<char> >&)'
     foo(c);
          ^
main.cpp:17:6: note: candidate: template<class T> void foo(const std::shared_ptr<A<T> >&)
 void foo(const std::shared_ptr<A<T>>& a)
      ^~~
main.cpp:17:6: note:   template argument deduction/substitution failed:
main.cpp:26:10: note:   mismatched types 'A<T>' and 'C<char>'
     foo(c);
          ^

The fix

So I changed foo to this:

template <class T, template <class> class U>
void foo(const std::shared_ptr<U<T>>& a)
{
    std::cout << "HI !" << std::endl;    
}

A better way ?

It works, but it doesn't feel right as the template constraints on the types are now really loose which usually end up into more obfuscated error message.

Is there a better way to handle these kind of template deduction failures ?

Play with the example on Coliru

http://coliru.stacked-crooked.com/a/d4ebc14105c184df

Upvotes: 2

Views: 94

Answers (3)

Jarod42
Jarod42

Reputation: 217085

In your example, you don't need to transfer ownership, so you should prefer to pass argument by const reference, and then it works as expected:

template <class T>
void foo(const A<T>& a)
{
    std::cout << "HI !" << std::endl;    
}

int main()
{    
    auto c = std::make_shared<C<char>>();

    foo(*c);
}

Demo

If you really need to pass shared_ptr, see the other answer.

Upvotes: 1

max66
max66

Reputation: 66190

I propose a little improvement of your foo (the template template one) where the class receive a list of template type parameter; with SFINAE can activated only if the C<T0, Ts...> class is a derived class of A<T0>.

template <template <typename...> class C, typename T0, typename ... Ts>
typename std::enable_if<std::is_base_of<A<T0>, C<T0, Ts...>>::value>::type
   foo (const std::shared_ptr<C<T0, Ts...>>& a)
 { std::cout << "HI !" << std::endl; }

So you can call foo() with A<T>, B<T, U> and C<T> but not with Z<T> or (by example) std::vector<T>

The following is a full compilable example

#include <memory>
#include <vector>
#include <iostream>

template <typename T>
struct A {};

template <typename T>
struct Z {};

template <typename T, typename U>
struct B : A<T>, Z<U> {};

template <typename T>
struct C : B<T, T> {};

template <template <typename...> class C, typename T0, typename ... Ts>
typename std::enable_if<std::is_base_of<A<T0>, C<T0, Ts...>>::value>::type
   foo (const std::shared_ptr<C<T0, Ts...>>& a)
 { std::cout << "HI !" << std::endl; }

int main ()
 {    
   foo(std::make_shared<A<char>>());
   foo(std::make_shared<B<char, long>>());
   foo(std::make_shared<C<char>>());
   // foo(std::make_shared<Z<char>>());           // compilation error
   // foo(std::make_shared<std::vector<char>>()); // compilation error
 }

Upvotes: 1

Curious
Curious

Reputation: 21510

There is a way. You can choose to constrain the template manually with a bit of template metaprogramming so that only pointer types (can be extended to work with all sorts of pointer types, but only works with both unique_ptr and shared_ptr in this case (actually with all smart pointer types that have an element_type type alias)) of derived classes can be used.

#include <memory>
#include <iostream>

namespace {

    /**
     * Get the template type of a template template type
     */
    template <typename T>
    struct GetType;
    template <typename T, template <typename...> class TT>
    struct GetType<TT<T>> {
        using type = T;
    };
} // namespace <anonymous>

template <class T>
struct A {};

template <class T>
struct Z {};

template <class T, class U>
struct B : A<T>, Z<U> {};

template <class T>
struct C : B<T, T> {};

template <class T, typename std::enable_if_t<std::is_base_of<
    A<typename GetType<typename std::decay_t<T>::element_type>::type>,
    typename std::decay_t<T>::element_type>::value>* = nullptr>
void foo(const T&)
{
    std::cout << "HI !" << std::endl;
}

int main()
{
    auto c = std::make_shared<C<char>>();
    auto c_uptr = std::make_unique<C<char>>();

    foo(c);
    foo(c_uptr);

    return 0;
}

This will now work both with unique_ptr and shared_ptr. You can chose to make it more general by checking the type of pointer contained (element_type in the example) by using decltype with .get() on the smart pointer, and make a trait that detects if the pointer passed is a primitive pointer and if so just uses the type itself. But you get the point.

Upvotes: 1

Related Questions