作为模板参数传递的函数


224

我正在寻找涉及将C ++模板函数作为参数传递的规则。

C ++支持此功能,如此处的示例所示:

#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>();
}

但是,学习这种技术很困难。谷歌搜索“作为模板参数的功能”并没有多大帮助。令人惊讶的是,经典的C ++模板《完整指南》也没有对此进行讨论(至少不是从我的搜索中获得)。

我的问题是这是否有效的C ++(或只是一些广泛支持的扩展)。

另外,有没有办法在这种模板调用期间允许具有相同签名的仿函数与显式函数互换使用?

下面确实没有工作,在上面的程序,至少在视觉C ++,因为语法显然是错误的。能够为functor切换一个函数,反之亦然,这很不错,类似于如果您要定义自定义比较操作,可以将函数指针或functor传递给std :: sort算法。

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

    doOperation<add3>();

指向一个或两个Web链接或C ++ Templates书中的页面的指针将不胜感激!


函数作为模板参数的好处是什么?返回类型不能用作模板类型吗?
daClown

相关:没有捕获的lambda可以衰减为函数指针,您可以在C ++ 17中将其作为模板参数传递。Clang可以正常编译,但是当前的gcc(8.2)有一个错误,即使使用,也将其错误地拒绝为“没有链接” -std=gnu++17我可以将C ++ 17不捕获lambda constexpr转换运算符的结果用作函数指针模板非类型参数吗?
彼得·科德斯

Answers:


123

是的,它是有效的。

至于使其也可以与函子一起使用,通常的解决方案是这样的:

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

现在可以称为:

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

现场观看

这样做的问题在于,如果使编译器内联调用变得棘手add2,因为所有编译器都知道函数指针类型void (*)(int &)正在传递给doOperation。(但是add3,作为函子,可以很容易地内联。在这里,编译器知道将类型的对象add3传递给函数,这意味着要调用的函数是add3::operator(),而不仅仅是一些未知的函数指针。)


19
现在,这是一个有趣的问题。当传递一个函数名时,就不会涉及到函数指针。这是一个显式函数,在编译时给出。因此,编译器确切地知道在编译时得到了什么。
SPWorley

1
在函数指针上使用函子有一个优势。函子可以在类内部实例化,从而为编译器提供了更多的优化性(例如内联)。编译器将很难被迫优化函数指针的调用。
马丁·约克

11
当在模板参数中使用该函数时,它将“衰减”为指向所传递函数的指针。与数组作为参数传递给参数时,数组如何衰减为指针无关。当然,指针值在编译时是已知的,并且必须指向具有外部链接的函数,以便编译器可以将此信息用于优化目的。
CB Bailey 2009年

5
快进几年后,在C ++ 11中使用函数作为模板参数的情况有了很大的改善。您不再必须像函子类那样使用Javaism,并且可以直接将例如静态内联函数用作模板参数。与1970年代的Lisp宏相比,仍然相去甚远,但是C ++ 11多年来确实取得了良好的进步。
pfalcon 2013年

5
因为c ++ 11最好将该函数用作右值引用(template <typename F> void doOperation(F&& f) {/**/}),所以例如bind可以传递bind-expression而不是绑定它?
user1810087 2015年

70

模板参数可以通过类型(类型名称T)或值(int X)进行参数化。

模板代码的“传统” C ++方法是使用函子-也就是说,代码位于对象中,因此对象为代码提供了唯一的类型。

当使用传统功能时,此技术不能很好地工作,因为类型的改变并不表示特定的功能-而是仅指定许多可能功能的签名。所以:

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);

不等于函子的情况。在此示例中,对签名为int X(int,int)的所有函数指针实例化do_op。编译器必须非常主动才能完全内联这种情况。(不过,由于编译器优化已经非常先进,因此我不排除它。)

一种表明该代码不能完全满足我们要求的方法是:

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

仍然是合法的,并且显然没有内联。要获得完整的内联,我们需要按值进行模板化,因此该功能在模板中完全可用。

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);

在这种情况下,每个实例化的do_op版本都使用已经可用的特定功能实例化。因此,我们希望do_op的代码看起来很像“ return a + b”。(Lisp程序员,请您不要傻笑!)

我们还可以确认这与我们想要的更接近,因为:

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

将无法编译。GCC表示:“错误:'func_ptr'无法出现在常量表达式中。换句话说,我无法完全扩展do_op,因为您在编译器时没有给我足够的信息来知道我们的操作是什么。

因此,如果第二个示例确实完全嵌入了我们的操作,而第一个示例则没有,那么模板有什么用?到底在做什么 答案是:键入强制。在第一个示例上的即兴演奏将起作用:

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);

该示例将起作用!(我并不是说这是一个好的C ++,但是...)发生的事情是do_op已围绕各种函数的签名进行了模板化,并且每个单独的实例化都将编写不同类型的强制代码。因此,使用fadd的do_op的实例化代码如下所示:

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.

相比之下,我们的按值大小写情况要求函数参数完全匹配。


2
请参阅stackoverflow.com/questions/13674935/…,以获取直接响应此处观察结果int c = do_op(4,5,func_ptr);的“显然没有内联” 的后续问题。
Dan Nissenbaum 2012年

请参阅此处的内联示例:stackoverflow.com/questions/4860762/…看来这些天编译器变得越来越聪明了。
BigSandwich

15

可以将函数指针作为模板参数进行传递,这是标准C ++的一部分 。但是,在模板中将它们声明为函数,而不是函数指针。在模板实例化时,一个传递函数的地址,而不仅仅是名称。

例如:

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);

如果要传递函子类型作为模板参数:

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);

有几个答案将函子实例作为参数传递:

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);

使用模板参数最接近这种统一外观的方法是定义do_op两次,一次是使用非类型参数,一次是使用类型参数。

// 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);

老实说,我真的希望它不会被编译,但是它对我来说适用于gcc-4.8和Visual Studio 2013。


9

在您的模板中

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

该参数T是非类型模板参数。这意味着模板函数的行为随参数值的变化而变化(该参数必须在编译时固定,即函数指针常量是固定的)。

如果您想同时使用功能对象和功能参数,则需要一个类型化的模板。但是,在执行此操作时,还需要在运行时为该函数提供一个对象实例(函数对象实例或函数指针)。

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

有一些较小的性能注意事项。此新版本使用函数指针参数的效率可能较低,因为仅在运行时取消引用和调用特定的函数指针,而基于使用的特定函数指针,可以优化函数指针模板(可能是内联的函数调用)。功能对象通常可以使用类型化的模板非常有效地扩展,尽管具体operator()情况完全取决于功能对象的类型。



0

编辑:将运算符作为参考传递不起作用。为简单起见,请将其理解为函数指针。您只是发送指针,而不是引用。我认为您正在尝试编写类似这样的内容。

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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.