Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template argument deduction from inherited type

I want a setup like the following:

template <typename T> class a {};

class b : public a<int> {};

template <typename T>
void do_foo(std::unique_ptr<a<T>> foo)
{
    // Do something with foo
}

int main()
{
    do_foo(std::make_unique<b>());
}

This fails to compile with a note saying template argument deduction/substitution failed and mismatched types 'a<T>' and 'b'. It's pretty self-explanatory. I can help the compiler along by writing do_foo<int>(std::make_unique<b>());, but then I'm repeating myself by writing int twice.

Is there a way to get the compiler to deduce the template parameter in this case? And what would you call this behaviour? I tried searching for things like "template type deduction for inherited type", "polymorphic template deduction" etc.

like image 491
jezza Avatar asked Oct 14 '22 22:10

jezza


1 Answers

Is there a way to get the compiler to deduce the template parameter in this case?

No. Not in C++14 (or even C++20).

And what would you call this behaviour?

Standard compliant. To be specific, this paragraph applies:

[temp.deduct.call]

4 In general, the deduction process attempts to find template argument values that will make the deduced A identical to A (after the type A is transformed as described above). However, there are three cases that allow a difference:

  • If the original P is a reference type, the deduced A (i.e., the type referred to by the reference) can be more cv-qualified than the transformed A.
  • The transformed A can be another pointer or pointer to member type that can be converted to the deduced A via a qualification conversion ([conv.qual]).
  • If P is a class and P has the form simple-template-id, then the transformed A can be a derived class of the deduced A. Likewise, if P is a pointer to a class of the form simple-template-id, the transformed A can be a pointer to a derived class pointed to by the deduced A.

This is an exhaustive list of cases where a template argument can be validly deduced from a function argument even if it doesn't match the pattern of the function parameter exactly. The first and second bullets deal with things like

template<class A1> void func(A1&){}
template<class A2> void func(A2*){}

int main() {
    const int i = 1;
    func(i); // A1 = const int
    func(&i); // A2 = const int
}

The third bullet is the one that is closest to our case. A class derived from a template specialization can be used to deduce a template parameter pertaining to its base. Why doesn't it work in your case? Because the function template parameter is unique_ptr<a<T>> and the argument you call it with is unique_ptr<b>. The unique_ptr specializations are not themselves related by inheritance. So they don't match the bullet, and deduction fails.

But it doesn't mean that a wrapper like unique_ptr prevents template argument deduction entirely. For instance:

template <typename> struct A {};
struct B : A<int> {};

template<typename> struct wrapper{};
template<> struct wrapper<B> : wrapper<A<int>> {};

template<typename T>
void do_smth(wrapper<A<T>>) {}

int main() {
    do_smth(wrapper<B>{});
}

In this case, wrapper<B> derives from wrapper<A<int>>. So the third bullet is applicable. And by the complex (and recursive) process of template argument deduction, it allows B to match A<T> and deduce T = int.

TL;DR: unique_ptr<T> specializations cannot replicate the behavior of raw pointers. They don't inherit from the specializations of unique_ptr over T's bases. Maybe if reflection ever comes to C++, we'll be able to meta-program a smart pointer that does behave that way.

like image 105
StoryTeller - Unslander Monica Avatar answered Oct 21 '22 09:10

StoryTeller - Unslander Monica