c++ 作为模板参数传递的函数

00jrzges  于 2023-02-06  发布在  其他
关注(0)|答案(7)|浏览(210)

我正在寻找将C模板函数作为参数传递的规则。
这是由C
支持的,如下面的示例所示:

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

然而,学习这项技术是很困难的,Googling for "function as a template argument"并没有带来太多的结果,而且令人惊讶的是,经典的C++ Templates The Complete Guide也没有讨论它(至少在我的搜索中没有)。
我的问题是这是否是有效的C++(或者只是一些广泛支持的扩展)。
另外,在这种模板调用过程中,是否有办法允许具有相同签名的函数与显式函数互换使用?
下面的代码在上面的程序中工作,至少在Visual C++中是这样,因为语法显然是错误的。如果能够切换出一个函子的函数,反之亦然,那就太好了,类似于如果你想定义一个自定义的比较操作,你可以将一个函数指针或函子传递给std::sort算法。

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

doOperation<add3>();

指向一两个web链接,或者C++模板书中的一个页面的指针将不胜感激!

8oomwypt

8oomwypt1#

是的,它是有效的。
至于让它也能和函子一起工作,通常的解决方案是这样的:

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

其现在可以被称为:

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

See it live
这样做的问题是,如果编译器内联对add2的调用会变得棘手,因为编译器所知道的只是void (*)(int &)类型的函数指针被传递给doOperation。(但是add3是一个函子,可以很容易地内联,这里,编译器知道一个add3类型的对象被传递给了函数,这意味着要调用的函数是add3::operator(),而不仅仅是某个未知的函数指针。)

lo8azlld

lo8azlld2#

模板参数可以按类型(typename 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);

并不等同于函子的情况。在这个例子中,do_op是为所有签名为int X(int,int)的函数指针示例化的。编译器必须非常积极地完全内联这种情况。(不过我不排除这种可能性,因为编译器优化已经相当先进了。)
判断这段代码没有完全达到我们的要求的一种方法是:

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已经围绕各种函数的 * signature * 进行了模板化,并且每个单独的示例化将编写不同的类型强制代码。因此,使用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.

相比之下,我们的按值情况要求函数参数精确匹配。

zysjyyx4

zysjyyx43#

函数指针可以作为模板参数和this is part of standard 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。

67up9zun

67up9zun4#

在模板中

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()完全由函数对象的类型确定。

r1zk6ea1

r1zk6ea15#

functor示例不起作用的原因是需要一个示例来调用operator()

j5fpnvbx

j5fpnvbx6#

这里有一个额外的要求,参数/返回类型也应该变化。按照Ben Supnik,这将是一些类型T

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

代替

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

这里的解决方案是将函数类型定义和函数模板放到周围的结构模板中。

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

或者BinOp可以是一个带有静态方法模板do_op(...)的类,然后调用为

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

编辑

受0x2207注解的启发,这里有一个函子,它接受任何带两个参数和可转换值的函数。

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

正确计算为 * double * 1.03和 * int * 1

evrscar2

evrscar27#

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

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;

相关问题