Search code examples
c++c++17template-argument-deduction

template argument deduction in nondeduced context


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.


Solution

  • 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