Function passed as template argument
Asked Answered
S

7

280

I'm looking for the rules involving passing C++ templates functions as arguments.

This is supported by C++ as shown by an example here:

#include <iostream>
void add1(int &v) { v += 1; } 
void add2(int &v) { v += 2; }

template <void (*T)(int &)>
void doOperation()
{
  int temp = 0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
    doOperation<add1>();
    doOperation<add2>();
}

Learning about this technique is difficult, however. Googling for "function as a template argument" doesn't lead to much. And the classic C++ Templates The Complete Guide surprisingly also doesn't discuss it (at least not from my search).

The questions I have are whether this is valid C++ (or just some widely supported extension).

Also, is there a way to allow a functor with the same signature to be used interchangeably with explicit functions during this kind of template invocation?

The following does not work in the above program, at least in Visual C++, because the syntax is obviously wrong. It'd be nice to be able to switch out a function for a functor and vice versa, similar to the way you can pass a function pointer or functor to the std::sort algorithm if you want to define a custom comparison operation.

struct add3 {
    void operator() (int &v) {v += 3;}
};
...

doOperation<add3>();

Pointers to a web link or two, or a page in the C++ Templates book would be appreciated!

Soosoochow answered 23/7, 2009 at 20:7 Comment(4)
What is the benefit of a function as template argument? Wouldn't the return type be used a the template type?Zeena
Related: a lambda with no captures can decay to a function pointer, and you can pass that as a template param in C++17. Clang compiles it ok, but current gcc (8.2) has a bug and incorrectly rejects it as having "no linkage" even with -std=gnu++17. Can I use the result of a C++17 captureless lambda constexpr conversion operator as a function pointer template non-type argument?.Reptile
thanks. I was trying to use template <void()(void) funcName>; but it turns out that templates use the old typedef style; (void)(*funcName)(void)Singlehandedly
@Zeena The benefit of a template arg is that it explicitly specifies that the value is instantiated at compile time and then remains constant during runtime.Danette
T
163

Yes, it is valid.

As for making it work with functors as well, the usual solution is something like this instead:

template <typename F>
void doOperation(F f)
{
  int temp = 0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

which can now be called as either:

doOperation(add2);
doOperation(add3());

See it live

The problem with this is that if it makes it tricky for the compiler to inline the call to add2, since all the compiler knows is that a function pointer type void (*)(int &) is being passed to doOperation. (But add3, being a functor, can be inlined easily. Here, the compiler knows that an object of type add3 is passed to the function, which means that the function to call is add3::operator(), and not just some unknown function pointer.)

Toney answered 23/7, 2009 at 20:12 Comment(17)
Now here's an interesting question. When passed a function name, it's NOT like there's a function pointer involved. It's an explicit function, given at compile time. So the compiler knows exactly what it's got at compile time.Soosoochow
Could a template overload be used to make the function pointer and the functor cases have identical syntax? Would there be any performance benefit? Especially for stateless functors which effectively have no instances and therefore we want to avoid any case where the compiler may stop inlining and optimization.Soosoochow
There is an advantage to using functors over function pointers. The functor can be instanciated inside the class and thus provides more opertunity to the compiler for optimizations (such as inlining). The compiler would be hard pressed to optimize a call over a function pointer.Arda
When the function is used in a template parameter it 'decays' into a pointer to the passed function. It's analagous to how arrays decay into pointers when passed as arguments to parameters. Of course, the pointer value is known at compile time and must point to a function with external linkage so that compiler can use this information for optimization purposes.Seamark
My comment above refers to template parameters taking a non-type function pointer parameter. In this case the template instantiation can be inlined as the compiler knows the value of the pointer at compile time. In jalf's example the template takes a type parameter and the type of the template parameter and the value of the function argument together determine the function called, a function object can be better optimized.Seamark
@Arno: For optimization purposes, I'd go with the functor, whether or not it is stateless.Toney
@Charles, so actually such a template is really passing a function POINTER? But that's really bizarre.. you can't make a template that takes a constant class instance pointer, for example. And if it's a pointer, than it's opaque to the compiler and it could never inline.Soosoochow
The template in your example is specialized for a function pointer, yes. But the value of that pointer is known at compile-time, so in principle it can still be inlined. I don't know if compilers actually do inline it though.Toney
Fast forward to few years later, situation with using functions as template arguments much improved in C++11. You no longer bound to use Javaisms like functor classes, and can use for example static inline functions as template arguments directly. Still far cry comparing to Lisp macros of 1970s, but C++11 definitely has good progress over the years.Escaut
@Soosoochow its NOT a pointer, except in C++, functions are equivalent to their pointers, and their pointers are equivalent to the pointers of their pointers. Which makes that all expressions involving functions are equivalent. MyFunc == &MyFunc == &&&&MyFunc == ***&MyFunc etcCrane
since c++11 wouldn't it be better to take the function as rvalue reference (template <typename F> void doOperation(F&& f) {/**/}), so bind for example can pass a bind-expression instead of binding it?Packer
@Escaut - I surely wish you'd expand a bit on your comment above "can use for example static inline functions as template arguments directly". Can you explain or point to an example or document? I'd like to know more.Jeneejenei
I learned about this by accident when reading the cppreference article about nullptr. en.cppreference.com/w/cpp/language/nullptr. It's a short example of passing a function pointer to a template.Cellulose
@Toney I am sorry that I am refreshing old topic, but what about static functors? Let's say that void operator() (int &v) inside struct add3, is a static function. Can I write; doOperation(add3::operator()); and be sure that compiler will be able to inline this static functor without a trouble?Julide
Doesn't doOperation(add3()); already call operator() on the functor? Surely, that's not what you want?Ostrom
@Ostrom I think the paren pair after "add3" is to contruct the instance of the add3 struct that is passed.Unbodied
@Escaut https://mcmap.net/q/110241/-what-are-the-new-ways-to-pass-functions-as-template-arguments-in-c-11/430766Impend
S
85

Template parameters can be either parameterized by type (typename T) or by value (int X).

The "traditional" C++ way of templating a piece of code is to use a functor - that is, the code is in an object, and the object thus gives the code unique type.

When working with traditional functions, this technique doesn't work well, because a change in type doesn't indicate a specific function - rather it specifies only the signature of many possible functions. So:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

Isn't equivalent to the functor case. In this example, do_op is instantiated for all function pointers whose signature is int X (int, int). The compiler would have to be pretty aggressive to fully inline this case. (I wouldn't rule it out though, as compiler optimization has gotten pretty advanced.)

One way to tell that this code doesn't quite do what we want is:

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

is still legal, and clearly this is not getting inlined. To get full inlining, we need to template by value, so the function is fully available in the template.

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

In this case, each instantiated version of do_op is instantiated with a specific function already available. Thus we expect the code for do_op to look a lot like "return a + b". (Lisp programmers, stop your smirking!)

We can also confirm that this is closer to what we want because this:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

will fail to compile. GCC says: "error: 'func_ptr' cannot appear in a constant-expression. In other words, I can't fully expand do_op because you haven't given me enough info at compiler time to know what our op is.

So if the second example is really fully inlining our op, and the first is not, what good is the template? What is it doing? The answer is: type coercion. This riff on the first example will work:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

That example will work! (I am not suggesting it is good C++ but...) What has happened is do_op has been templated around the signatures of the various functions, and each separate instantiation will write different type coercion code. So the instantiated code for do_op with fadd looks something like:

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

By comparison, our by-value case requires an exact match on the function arguments.

Setup answered 23/7, 2009 at 20:7 Comment(2)
See #13675435 for a follow-up question in direct response to the observation here that int c = do_op(4,5,func_ptr); is "clearly not getting inlined".Tiernan
See here for an example of this being inlined: #4861262 Seems that compilers are getting pretty smart these days.Sorcim
T
30

Function pointers can be passed as template parameters, and this is part of standard C++ . However in the template they are declared and used as functions rather than pointer-to-function. At template instantiation one passes the address of the function rather than just the name.

For example:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

If you want to pass a functor type as a template argument:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

Several answers pass a functor instance as an argument:

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

The closest you can get to this uniform appearance with a template argument is to define do_op twice- once with a non-type parameter and once with a type parameter.

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

Honestly, I really expected this not to compile, but it worked for me with gcc-4.8 and Visual Studio 2013.

Tirrell answered 1/3, 2014 at 8:17 Comment(0)
S
11

In your template

template <void (*T)(int &)>
void doOperation()

The parameter T is a non-type template parameter. This means that the behaviour of the template function changes with the value of the parameter (which must be fixed at compile time, which function pointer constants are).

If you want somthing that works with both function objects and function parameters you need a typed template. When you do this, though, you also need to provide an object instance (either function object instance or a function pointer) to the function at run time.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

There are some minor performance considerations. This new version may be less efficient with function pointer arguments as the particular function pointer is only derefenced and called at run time whereas your function pointer template can be optimized (possibly the function call inlined) based on the particular function pointer used. Function objects can often be very efficiently expanded with the typed template, though as the particular operator() is completely determined by the type of the function object.

Seamark answered 23/7, 2009 at 20:18 Comment(0)
C
3

The reason your functor example does not work is that you need an instance to invoke the operator().

Copy answered 23/7, 2009 at 20:12 Comment(0)
H
2

Came here with the additional requirement, that also parameter/return types should vary. Following Ben Supnik this would be for some type T

typedef T(*binary_T_op)(T, T);

instead of

typedef int(*binary_int_op)(int, int);

The solution here is to put the function type definition and the function template into a surrounding struct template.

template <typename T> struct BinOp
{
    typedef T(*binary_T_op )(T, T); // signature for all valid template params
    template<binary_T_op op>
    T do_op(T a, T b)
    {
       return op(a,b);
    }
};


double mulDouble(double a, double b)
{
    return a * b;
}


BinOp<double> doubleBinOp;

double res = doubleBinOp.do_op<&mulDouble>(4, 5);

Alternatively BinOp could be a class with static method template do_op(...), then called as

double res = BinOp<double>::do_op<&mulDouble>(4, 5);

EDIT

Inspired by comment from 0x2207, here is a functor taking any function with two parameters and convertible values.

struct BinOp
{
    template <typename R, typename S, typename T, typename U, typename V> R operator()(R (*binaryOp )(S, T), U u, V v)
    {
        return binaryOp(u,v);
    }

};

double subD(double a, int b)
{
    return a-b;
}

int subI(double a, int b)
{
    return (int)(a-b);
}


int main()
{
    double resD = BinOp()(&subD, 4.03, 3);
    int resI = BinOp()(&subI, 4.03, 3);

    std::cout << resD << std::endl;
    std::cout << resI << std::endl;
    return 0;
}

correctly evaluates to double 1.03 and int 1

Hake answered 18/8, 2020 at 12:58 Comment(1)
Is it anyhow possible to derive T from &mulDuouble type? Because now you have to specify double twice.Barbaresi
S
0

Edit: Passing the operator as a reference doesnt work. For simplicity, understand it as a function pointer. You just send the pointer, not a reference. I think you are trying to write something like this.

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

. .

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
Salinas answered 23/7, 2009 at 20:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.