c++stdmetaprogrammingtemplate-meta-programmingc++23

Why template instantiation requires specialation in the case where it is already done for all cases?


In c++ when i try to write a code that returns reverse of a type list what i wrote( a user defined template class):

template <typename ...V,typename ...D>
    constexpr decltype(auto) merge(const type_list<V...>& first, const type_list<D...>& second)
    {
        return type_list<V...,D...>();
    }

    template<typename First, typename ...Var>
    constexpr decltype(auto) _reverse(type_list<First,Var...> ls)
    {
        if constexpr (empty(ls)) return type_list<>(); 
        return merge(_reverse(type_list<Var...>()),type_list<First>()); 
    }

and when i try to run:

aml::type_list<int,float> ls;
auto x = aml::_reverse(ls);

clang says:

In file included from test.cpp:4:
././lesson_4.hpp:64:16: error: no matching function for call to '_reverse'
   64 |                 return merge(_reverse(type_list<Var...>()),type_list<First>()); 
      |                              ^~~~~~~~
././lesson_4.hpp:64:16: note: in instantiation of function template specialization 'aml::_reverse<float>' requested here
test.cpp:10:16: note: in instantiation of function template specialization 'aml::_reverse<int, float>' requested here
   10 |         auto x = aml::_reverse(ls);
      |                       ^
././lesson_4.hpp:61:27: note: candidate template ignored: failed template argument deduction
   61 |         constexpr decltype(auto) _reverse(type_list<First,Var...> ls)
      |                                  ^
1 error generated.

Could you please explain me why compiler just can't instantiate for example reverse<int,int,float> function template <typename First = int, template ...Other = {int,float}>? And why it says another specialation is needed like <int,float>? because i think they are also an instance of typename First, typename ...Other specialation


Solution

  • The problem is that your function doesn't actually cover all cases; it misses the case where type_list's template parameter pack is empty. And that's a pretty important case, since it's the one _reverse() falls back on to end its recursion!

    Ultimately, there are two ways you can fix it. You can either provide a separate overload to catch the empty parameter pack, or you can modify _reverse() to check if Var... is empty instead. The first would look like this:

    // Empty list overload.
    constexpr decltype(auto) _reverse(type_list<> ls) {
        return type_list<>();
    }
    
    // Your _reverse().
    template<typename First, typename ...Var>
    constexpr decltype(auto) _reverse(type_list<First,Var...> ls)
    {
        if constexpr (empty(ls)) return type_list<>(); 
        return merge(_reverse(type_list<Var...>()),type_list<First>()); 
    }
    

    And the second would look like this:

    // Modified _reverse().
    // Note the compound if constexpr.  Branches must be mutually exclusive, the first branch WILL
    //  fail to compile if the merge() call is visible.  if constexpr shields `type_list<Var...>` from
    //  empty `Var...`, to prevent invalid `_reverse(type_list<>())` call.
    template<typename First, typename... Var>
    constexpr decltype(auto) _reverse(type_list<First, Var...> ls) {
        // Either check will work here; one requires type_list::size(), one doesn't.
        //if constexpr (size(ls) == 1) {     // Requires type_list::size().
        if constexpr (sizeof...(Var) == 0) { // Uses Var... pack's size directly.
            return type_list<First>();
        } else {
            return merge(_reverse(type_list<Var...>()), type_list<First>());
        }
    }
    

    You can see both versions here, the #if lets you choose between them.