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

81 Views Asked by At

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.

2

There are 2 best solutions below

0
Ted Lyngmo On BEST ANSWER

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";
}
0
n. m. could be an AI On

Keeping the implementation in line with what you already have, you can do this:

  template<template<typename> typename Tab> struct AB final
  {
      AB() = delete;
  };
  
  template<> struct AB<a::A> final
  {
      AB() = delete;
      using type = a::A<float>;
  };
  
  template<> struct AB<b::B> final
  {
      AB() = delete;
      using type = b::B<float>;
  };  
        
  template<template <typename> typename Tab, typename = typename AB<Tab>::type>
  std::string h(const Tab<float>&)
  {
      return "h(Tab<float>)\n";                        
  }  

and then what shouldn't compile, won't.

Note that the inner using type = a::A<float>; is only used for identification. You still can write

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

without changing AB.

I personally would use static_assert or enable_if over this design. Still, it's an option worth having.