c++templatesc++14metaprogrammingtemplate-templates

how do I "tighten up" arguments to a templatized function?


I have two related types (duck-typing), and another one that provides similar functionality with a different interface:

namespace a
{
    template<typename T>
    struct A final {};

    using int_t = A<int>;
    using float_t = A<float>;
}
namespace b
{
    template<typename T>
    struct B final {};

    using int_t = B<int>;
    using float_t = B<float>;
}

namespace foo
{
    template<typename T>
    struct Foo final {};

    using int_t = Foo<int>;
    using float_t = Foo<float>;

    std::string f(const int_t&)
    {
        return "f(Foo<int>)\n";
    }
    std::string g(const int_t&)
    {
        return "g(Foo<int>)\n";
    }
    std::string h(const float_t&)
    {
        return "h(Foo<float>)\n";
    }
}

a and b are "related", foo is "similar." Writing a generic f() works, but is too "greedy":

template<typename T>
std::string f(const T&)
{
    return "f(T)\n";
}

// ...

int main()
{
    a::int_t a_int;
    b::int_t b_int;
    foo::int_t foo_int;

    std::cout << f(a_int); // "f(T)"
    std::cout << f(b_int); // "f(T)"
    std::cout << f(foo_int); // "f(Foo<int>)"
    std::cout << f(314); // NO! ... "f(T)"
    std::cout << "\n";
}

To "tighten" that up a bit, I did the following:

template<typename Tab> struct AB final
{
    AB() = delete;
};
template<typename T> struct AB<a::A<T>> final
{
    AB() = delete;
    using type = a::A<T>;
};
template<typename T> struct AB<b::B<T>> final
{
    AB() = delete;
    using type = b::B<T>;
};

Now I can write g() that only works on a::A<T> and b:B<T>:

template<typename Tab, typename type_ = typename AB<Tab>::type>
std::string g(const Tab&)
{
    return "g(Tab)\n";
}

This works as expected:

    std::cout << g(a_int); // "g(Tab)"
    std::cout << g(b_int); // "g(Tab)"
    std::cout << g(foo_int); // "g(Foo<int>)"
    //std::cout << g(314); // YES! doesn't compile 
    std::cout << "\n";

However, I can't figure out how to set things up to write an h() that will only work for either a::A<float> or b::B<float>, but not any other specialization.

template<typename Tab, typename type_ = typename AB<Tab>::type>
std::string h(const Tab&)
{
    return "h(Tab<float>)\n";
}

// ...

    a::float_t a_float;
    b::float_t b_float;
    foo::float_t foo_float;

    std::cout << h(a_float); // "h(Tab<float>)";
    std::cout << h(b_float); // "h(Tab<float>)";
    std::cout << h(foo_float); // "h(Foo<float>)"
    std::cout << h(a_int); // NO! ... "h(Tab<float>)";
    std::cout << h(b_int); // NO! ... "h(Tab<float>)";
    //std::cout << h(foo_int); // YES! doesn't compile 
    //std::cout << h(314); // YES! doesn't compile 

I've tried a passing a template as a template, but I can't quite figure out the right syntax.

I'd like something that works in C++14.


Solution

  • I can't figure out how to set things up to write an h() that will only work for either a::A<float> or b::B<float>, but not any other specialization.

    You could add a constraint.

    C++20:

    template <template <class> class Tab, class T>
        requires(std::same_as<a::float_t, Tab<T>> ||
                 std::same_as<b::float_t, Tab<T>>)
    std::string h(const Tab<T>&) {
        return "h(Tab<float>)\n";
    }
    

    C++14:

    template <template <class> class Tab, class T>
    std::enable_if_t<std::is_same<a::float_t, Tab<T>>::value ||
                     std::is_same<b::float_t, Tab<T>>::value,
                     std::string>
    h(const Tab<T>&) {
        return "h(Tab<float>)\n";
    }
    

    In both versions, you make the function take a template-template parameter (Tab) and then check if Tab<T> is either a::float_t or b::float_t SFINAE style to allow for other h overloads.


    If you don't need SFINAE to create other h overloads, a static_assert may be preferable:

    template <template <class> class Tab, class T>
    std::string h(const Tab<T>&) {
        static_assert(std::is_same<a::float_t, Tab<T>>::value ||
                      std::is_same<b::float_t, Tab<T>>::value,
                      "must be a::float_t or b::float_t");
        return "h(Tab<float>)\n";
    }