Template deduction with derived templated class and smart pointers

66 views Asked by At

Let's say I have a templated base class, and a templated class that derives from it:

template <typename T>
class Base {};

template <typename T>
class Derived : public Base<T> {};

Further, I've got a function that wants to accept a shared pointer to any Base<T> or subclass, and be able to easily use the T parameter as part of its signature:

template <typename T>
T DoSomething(std::shared_ptr<Base<T>>);

I want to be able to call it, with a deduced T, with shared pointers to Base<T> or anything that derives from it:

DoSomething(std::make_shared<Base<T>>());
DoSomething(std::make_shared<Derived<T>>());

Of course the latter doesn't work, because type deduction fails.

How can I modify the signature of DoSomething to allow it to work? I've seen plenty of answers in the case where Base and Derived aren't templates, but I'm not sure how to do it if I still want to deduce T (for example to use it as a return type, as above).

Ideally this would fail for shared pointers to non-derived inputs (and non-shared pointers) at overload resolution time.

2

There are 2 answers

6
super On BEST ANSWER

You can use a template template parameter in your function, then a static_assert to enforce your requirement.

#include <memory>
#include <vector>

template <typename T>
class Base {};

template <typename T>
class Derived : public Base<T> {};

template <typename T, template <typename> typename U>
T DoSomething(std::shared_ptr<U<T>>) {
    static_assert(std::is_base_of_v<Base<T>, U<T>>, "Requires a std::shared_ptr to Base of class derived from Base");
    return T{};
}

int main() {
    auto foo = std::make_shared<Derived<int>>();
    auto baz = std::make_shared<Base<int>>();
    auto bar = std::make_shared<std::vector<int>>();

    DoSomething(foo);
    DoSomething(baz);
    DoSomething(bar); // Fails, std::vector<int> is not derived from Base<int>
}

Edit
If DoSomething is overloaded we can use SFINAE to disable it instead of the static_assert. That would look as follows.

#include <memory>
#include <vector>

template <typename T>
class Base {};

template <typename T>
class Derived : public Base<T> {};

template <typename T, template <typename> typename U, std::enable_if_t<std::is_base_of_v<Base<T>, U<T>>, int> = 0>
T DoSomething(std::shared_ptr<U<T>>) {
    return T{};
}

int main() {
    auto foo = std::make_shared<Derived<int>>();
    auto baz = std::make_shared<Base<int>>();
    auto bar = std::make_shared<std::vector<int>>();

    DoSomething(foo);
    DoSomething(baz);
    DoSomething(bar); // Error, no matching function
}
0
StefanKssmr On

Even though, there is already an accepted answer to this problem by @super, here is a slightly different (but probably less elegant way) of using SFINAE:

#include <memory>
#include <type_traits>

template <typename T>
class Base {};

template <typename T>
class Derived : public Base<T> {};

template <template <typename> typename U, typename T>
std::enable_if_t<std::is_base_of<Base<T>,U<T>>::value, T>
DoSomething(std::shared_ptr<U<T>>)
{
    return T{};
}

int main()
{
    auto foo = std::make_shared<Base<int>>();
    auto bar = std::make_shared<Derived<int>>();
    
    DoSomething(foo);
    DoSomething(bar);

    return 0;
}

Live demo: godbolt.

Why do I consider this less elegant? This is because the signature of the function changed and now has std::enable_if_t<...> as return type, even though it is finally deduced as T (for valid input, of course).