0

The following (simplified) code does not compile because the compiler cannot deduce a template parameter.

#include <iostream>
#include <utility>
#include <vector>

enum class animal : size_t { cat, dog };

template <animal a, class... U>
using animal_select_t = typename std::tuple_element<static_cast<size_t>(a), std::tuple<U...>>::type;

template <animal a>
using mytype = animal_select_t<a, int, double>;

template <animal a>
void print(std::vector<mytype<a>>&& vec)
{
  for (const auto& x : vec)
    std::cout << x << std::endl;
  vec.resize(0);
}

int main()
{
  std::vector<int> v(3, 1);
  print(std::move(v));
  std::cout << v.size() << std::endl;

  return 0;
}

The error message is

deduce.cpp: In function ‘int main()’:
deduce.cpp:24:8: error: no matching function for call to ‘print(std::remove_reference<std::vector<int>&>::type)’
   24 |   print(std::move(v));
      |   ~~~~~^~~~~~~~~~~~~~
deduce.cpp:14:6: note: candidate: ‘template<animal a> void print(std::vector<typename std::tuple_element<static_cast<long unsigned int>(a), std::tuple<int, double> >::type>&&)’
   14 | void print(std::vector<mytype<a>>&& vec)
      |      ^~~~~
deduce.cpp:14:6: note:   template argument deduction/substitution failed:
deduce.cpp:24:8: note:   couldn’t deduce template parameter ‘a’
   24 |   print(std::move(v));

Apparently, this is a case of a nondeduced context.

Obviously, reducing the "nesting" of template parameters solves the issue. For example, the print routine can be modified as

template <typename U>
void print(std::vector<U>&& vec)
{
  // which animal?
  for (const auto& x : vec)
    std::cout << x << std::endl;
  vec.resize(0);
}

However, this solution is not optimal because

  • print can be potentially instantiated with a std::vector of a type which is not one of the choice given in mytype

  • In an actual code, I may need to know the actual template parameter animal a.

Assuming we use print as in the second block of code, how can one:

  • restrict the instantiation to the allowed types, mytype<a>, with a an animal?

  • deduce the template parameter animal?

Or is there a better solution, implementing differently print?

Please notice that I am looking for a scalable solution. Obviously for the example above, the solution is easy, given that only int and double are considered. I am instead looking for a solution that I can generically use even when the enum class animal and/or the type alias mytype is changed. Also, I am looking for a solution in the c++17 standard.

francesco
  • 7,189
  • 7
  • 22
  • 49
  • The compiler isn't going to think how to reverse `mytype` to map type back to enum. It's far from trivial, and in some cases there might not be a 1-to-1 mapping at all. If you want the type-to-enum mapping, you need to write it manually. *"restrict the instantiation to the allowed types"* You need to write something like `template constexpr bool is_animal = ...;`, then plop it into a `std::enable_if_t` on `print`. – HolyBlackCat Jun 22 '23 at 17:31
  • *"I am looking for a scalable solution"* Can you tell more about what you're doing? "Scalable solution" and a hardcoded enum don't go hand in hand. For me, "scalable" is something that would let you define your animal classes in completely separate translation units, and assign indices to them at program startup (if you need indices at all). – HolyBlackCat Jun 22 '23 at 17:32
  • when you pass a `std::vector` to `print` then the `U` in `std::vector` is `int`. Its not clear what you mean with "I may need to know the actual template parameter animal a" – 463035818_is_not_an_ai Jun 22 '23 at 17:44
  • @463035818_is_not_an_ai In the first snippet, `print` has a template parameter `animal a`. – Nelfeal Jun 22 '23 at 18:31

1 Answers1

1

You can do all sorts of things if you can make a parameter pack of the list of enumerators in your enum:

template <animal... as>
struct animal_list {};

// needs to be kept updated with the definition of the enum class animal
using all_animals = animal_list<animal::cat, animal::dog>;

Once you have that, you can unpack the parameter pack and use for example a fold expression to check which mytype instantiation corresponds to a particular type:

template <typename T, template <animal> typename Selector, animal... as>
constexpr auto animal_from_type_helper(animal_list<as...> list) -> animal {
    animal a{};
    static_cast<void>(((a = as, std::is_same_v<Selector<as>, T>) || ...));
    return a;
}

template <typename T>
constexpr auto animal_from_type() -> animal {
    return animal_from_type_helper<T, mytype>(all_animals{});
}

static_assert(animal_from_type<int>() == animal::cat);
static_assert(animal_from_type<double>() == animal::dog);

You can do something similar to check if any instantiation of mytype has a particular type (in other words, if T is in the list animal_select_t<a, int, double>)

template <typename T, template <animal> typename Selector, animal... as>
constexpr bool is_allowed_helper(animal_list<as...> list) {
    return (std::is_same_v<Selector<as>, T> || ...);
}

template <typename T>
constexpr bool is_allowed() {
    return is_allowed_helper<T, mytype>(all_animals{});
}

Use this in a static_assert to restrict what types can be passed to print:

template <typename U>
void print(std::vector<U>&& vec)
{
    static_assert(is_allowed<U>());
    animal a = animal_from_type<U>();
}

Demo

Nelfeal
  • 12,593
  • 1
  • 20
  • 39