简体   繁体   中英

Can a templated function be a template argument to another function?

I thought I'd benchmark some sorting algorithms, but I must be doing templates incorrectly:

Code

#include <iostream>
#include <vector>

template <typename ForwardIterator>
void dummysort(ForwardIterator begin_it, ForwardIterator end_it)
{
    // pretend to use these and sort stuff
    ++begin_it;
    ++end_it;
}

template<typename SortFunc>
void benchmark(const char* name, SortFunc sort_func, std::vector<int> v)
{
    std::cout << name << std::endl;
    sort_func(v.begin(), v.end());
}

int main()
{
    std::vector<int> first = {3, 2, 1};

    benchmark("bubblesort", dummysort, first);
}

Error

10:48 $ clang -std=c++14 tmp.cpp
tmp.cpp:30:5: error: no matching function for call to 'benchmark'
    benchmark("bubblesort", dummysort, first);
    ^~~~~~~~~
tmp.cpp:20:6: note: candidate template ignored: couldn't infer template argument 'SortFunc'
void benchmark(const char* name, SortFunc sort_func, std::vector<int> v)
     ^
1 error generated.

Compiler Info

10:47 $ clang --version
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.0.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/

If I "untemplate" dummysort , it works.

void dummysort(std::vector<int>::iterator begin_it, std::vector<int>::iterator end_it)

Question

Is there a way I can make it work generically, or, if not, can someone please give me a good explanation or thought experiment similar to this answer ?

As explained by nwp, a template function isn't a function; is only a recipe to construct a function.

So you can't pass the recipe

benchmark("bubblesort", dummysort, first);

but you can do it explicating the type an constructing a function from the recipe; I mean

benchmark("bubblesort", dummysort<std::vector<int>::iterator>, first);

Works but I find it a little ugly.

I propose you another solution: instead of a template function, construct a class (or struct) with a template operator() in it; something like

struct dummySort
 {
   template <typename FI>
   void operator() (FI begin_it, FI end_it)
    {
      ++begin_it;
      ++end_it; 
    }
 };

In this way you can call benchmark() passing an object of this type without explicating the type of the iterator: it's the call to operator inside of benchmark() that select the right type and construct the right operator.

The following is a full compiling example

#include <iostream>
#include <vector>

struct dummySort
 {
   template <typename FI>
   void operator() (FI begin_it, FI end_it)
    { ++begin_it; ++end_it; }
 };

template <typename SortFunc>
void benchmark (char const * name, SortFunc sort_func, std::vector<int> v)
 {
   std::cout << name << std::endl;
   sort_func(v.begin(), v.end());
 }

int main()
 {
   std::vector<int> first = {3, 2, 1};

   benchmark("bubblesort", dummySort{}, first);
 }

There are a few ways around this problem.

  1. You could explicitly specify the template arguments for dummysort or you could static_cast it to the appropriate function pointer type. This, to me, is a pretty bad solution as it locks into a specific overload choice rather than relying on the language's overload rules. If you change other arguments or types, you could get (best-case) broken code or (worst-case) code that works but does something unexpected

  2. You could lift your function template to be a function object that has a call operator template:

     struct dummysort_t { template <typename ForwardIterator> void operator()(ForwardIterator begin_it, ForwardIterator end_it) const { /* ... */ } } constexpr dummysort{}; // in C++17, also mark inline 

    This allows you to just directly pass in dummysort to your function templates everywhere with the same expected behavior. The downsides to this solution are if you don't own dummysort , this is of course a non-starter, and if you use dummysort as a customization point, you need to do quite a bit more work to get this to have the expected behavior.

  3. You could wrap it in a lambda. Since we're copying iterators, the simple form just works:

     [](auto b, auto e) { dummysort(b, e); } 

    This delays template instantiation for dummysort until the other function template actually invokes it. This gives you the exact behavior you want, and will work in all situations (and fixes the issues with #2). More generally, this pattern should be used like:

     #define OVERLOADS_OF(name) [&](auto&&... args) -> decltype(name(std::forward<decltype(args)>(args)...)) { return name(std::forward<decltype(args)>(args)...); } 

    with which you would pass:

     OVERLOADS_OF(dummysort) 

    which for this particular case is overkill, but is more generally useful.

Two minor comments: You should pass the v parameter to benchmark by reference std::vector<int> &v and not by value std::vector<int> v , since vectors are expensive objects to construct. And std::endl is unnecessary with the standard streams; instead of

std::cout << "Value of the variable:" << std::endl;
std::cout << variable << std::endl;

you can just write

std::cout << "Value of the variable:\n";
std::cout << variable << '\n';

Which is both a lot less verbose and faster (not that it matters for the trivial case here).

To answer your question, no, the compiler would not be able to infer dummysort 's template parameter in this case, so you would have to write it dummysort<std::vector<int>::iterator> . But there are ways you can simplify it:

1) If your compiler supports C++14 variable templates you can declare

template<typename T>
constexpr auto vecdummysort = dummysort<typename std::vector<T>::iterator>;

And then use it like

benchmark("bubblesort", vecdummysort<int>, first);

2a) With C++11 you can create an alias template

template<typename T>
using veciter = typename std::vector<T>::iterator;

Which lets you use dummysort like

benchmark("bubblesort", dummysort<veciter<int>>, first);

2b) For the same vecdummysort function as in example #1:

template<typename T>
void vecdummysort(veciter<T> first, veciter<T> last) {dummysort(first, last);}

3) You could also change the call to sort_func in benchmark to use raw pointers instead of iterators, so

sort_func(v.data(), v.data() + v.size());

Instead of

sort_func(v.begin(), v.end());

Which means you would be able to use benchmark like

benchmark("bubblesort", dummysort<int*>, first);

4) You can change the interface of dummysort to take the collection itself as the argument instead of iterators, which sacrifices a tiny bit of flexibility (eg if you wanted to sort only part of the collection, or use reverse iterators, and so forth) for a less verbose and more ergonomic interface

template<typename Collection>
void dummysort(Collection &c)
{
    auto first = std::begin(c);
    auto last = std::end(c);
}

Usage:

// benchmark()
sort_func(v);
// main()
benchmark("bubblesort", dummysort<std::vector<int>>, first);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM