Search code examples
c++templatesvariadic-templates

Why does my variadic template instantiation not work?


I am revisiting C++ after a long hiatus, and as an exercise I would like to implement the known "map" function -- the one which applies a function to every element of a collection -- using templates.

Disregarding the fact my map doesn't return anything (a non-factor here), I have managed to implement what I wanted, with the limitation that the function passed to map may not accept additional arguments:

#include <iostream>

template <typename C, void fn(const typename C::value_type &)> void map(const C & c) {
    for(auto i : c) {
        fn(i);
    }
}

struct some_container_type { /// Just some hastily put together iterable structure type
    typedef int value_type;
    value_type * a;
    int n;
    some_container_type(value_type * a, int n): a(a), n(n) { }
    value_type * begin() const {
        return a;
    }
    value_type * end() const {
        return a + n; 
    }
};

void some_fn(const int & e) { /// A function used for testing the "map" function
    std::cout << "`fn` called for " << e << std::endl;
}

int main() {
    int a[] = { 5, 7, 12 };
    const some_container_type sc(a, std::size(a));
    map<some_container_type, some_fn>(sc);
}

However, I would like my map to accept additional arguments to pass to fn. I've tried to compile the modified variant of the program (container type definition was unchanged):

template <typename C, typename ... T, void fn(const typename C::value_type &, T ...)> void map(const C & c, T ... args) {
    for(auto i : c) {
        fn(i, args...);
    }
}

void some_fn(const int & e, int a, float b, char c) {
    std::cout << "`fn` called for " << e << std::endl;
}

int main() {
    int a[] = { 5, 7, 12 };
    const some_container_type sc(a, std::size(a));
    map<some_container_type, int, float, char, some_fn>(sc, 1, 2.0f, '3');
}

But gcc -std=c++20 refuses to compile the modified program containing the above variant, aborting with:

<source>: In function 'int main()':
<source>:29:56: error: no matching function for call to 'map<some_container_type, int, float, char, some_fn>(const some_container_type&, int, int, int)'
   29 |     map<some_container_type, int, float, char, some_fn>(sc, 1, 2, 3);
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
<source>:16:97: note: candidate: 'template<class C, class ... T, void (* fn)(const typename C::value_type&, T ...)> void map(const C&, T ...)'
   16 | template <typename C, typename ... T, void fn(const typename C::value_type &, T ... args)> void map(const C & c, T ... args) {
      |                                                                                                 ^~~
<source>:16:97: note:   template argument deduction/substitution failed:
<source>:29:56: error: type/value mismatch at argument 2 in template parameter list for 'template<class C, class ... T, void (* fn)(const typename C::value_type&, T ...)> void map(const C&, T ...)'
   29 |     map<some_container_type, int, float, char, some_fn>(sc, 1, 2, 3);
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
<source>:29:56: note:   expected a type, got 'some_fn'

Microsoft Visual C++ compiler (19.24.28314) gives a more descriptive error message:

error C3547: template parameter 'fn' cannot be used because it follows a template parameter pack and cannot be deduced from the function parameters of 'map'

Can someone explain if and how I can idiomatically accomplish for map to accept arbitrary arguments for forwarding these to fn?

I know I can pass fn to the map function as argument instead of specifying it as an argument to the template, but for reasons related to inlining and to better understand C++ templates, I'd like to retain fn a template rather than a function parameter.

I also don't want to use any libraries, including the standard library (what use of std I show in the examples above is only for clarifying the question). I know there are "functor" and "forward" somewhere in the libraries, but I suppose they too were written in C++, so I am curious if my problem can be solved without any libraries.


Solution

  • A simple way to fix this would be to deduce the non-type template parameter for the function, and reorder the template parameter list

    template <typename C, auto fn, typename ... T> 
    void map(const C & c, T ... args) {
        for(auto i : c) {
            fn(i, args...);
        }
    }
    

    and then call it like this

    map<some_container_type, some_fn, int, float, char>(sc, 1, 2.0f, '3');
    

    Here's a demo


    You could also move fn to the beginning of the template parameter list.

    template <auto fn, typename C, typename ... T> 
    void map(const C & c, T ... args) {
        for(auto i : c) {
            fn(i, args...);
        }
    }
    

    Now since C and T can be deduced from the function arguments, this makes the call site much cleaner

    map<some_fn>(sc, 1, 2.0f, '3');
    

    Here's a demo