函数对象调用和函数调用的详细区别是什么?

14

这种方法能够奏效的关键在于for_each()函数不必假设它的第三个参数是一个函数。 它只是假定第三个参数是可以用正确的参数调用的东西。 与函数一样,适当定义的对象同样适用,并且通常比函数更好。例如,内联类的应用程序运算符要比内联作为指向函数的指针传递的函数更容易。 因此,函数对象通常比普通函数执行得更快。 具有应用程序运算符(§11.9)的类的对象称为函数类对象、函数对象或者functor。

[Stroustrup, C++ 3rd edition, 18.4-last paragraph]

  1. 我一直认为操作符 ( ) 的调用就像运行时的函数调用。 它与普通函数调用有什么区别?

  2. 为什么内联应用程序运算符比普通函数更容易?

  3. 它们如何比函数调用更快?


相关:https://dev59.com/vHE95IYBdhLWcg3wWMdQ - Matteo Italia
3个回答

11

通常,函数对象(functor)被传递给模板函数 - 如果你这样做,那么无论你传递一个“真正”的函数(即函数指针)还是一个函数对象(即一个带有重载operator()的类),都不重要。本质上,两者都有一个函数调用运算符,因此都是有效的模板参数,编译器可以实例化for_each模板。这意味着for_each要么用传递的函数对象的特定类型实例化,要么用传递的函数指针的特定类型实例化。

在这种特化中,函数对象有可能胜过函数指针。毕竟,如果你传递一个函数指针,那么参数的编译时类型就是函数指针。如果for_each本身没有被内联,那么这个for_each实例就被编译成调用一个不透明的函数指针 - 毕竟,编译器怎么可能内联一个函数指针呢?它只知道它的类型,不知道传递的是哪个函数 - 至少在优化时,除非可以使用非局部信息,这很难做到。

然而,如果你传递一个函数对象,那么该函数对象的编译时类型将用于实例化for_each模板。这样做,你可能会传递一个简单的、非虚拟的类,只有一个适当的operator()的实现。因此,当编译器遇到对operator()的调用时,它知道指的是哪个实现 - 就是那个函数对象的唯一实现 - 现在它可以内联它。

如果你的函数对象使用虚方法,那么这种优势就消失了。当然,函数对象是一个类,你可以用它做很多其他低效的事情。但对于基本情况,这就是为什么编译器更容易优化并内联函数对象调用而不是函数指针调用的原因。

总结

函数指针无法进行内联,因为编译器在编译for_each时仅具有函数的类型而没有函数的标识。相比之下,函数对象可以进行内联,因为尽管编译器只有函数对象的类型,但该类型通常足以唯一标识函数对象的operator()方法。


5

考虑以下两个模板实例化:

std::for_each<class std::vector<int>::const_iterator, class Functor>(...)

并且

std::for_each<class std::vector<int>::const_iterator, void(*)(int)>(...)

由于第一个函数对象是针对每种类型的函数对象进行定制的,而且operator()通常是内联定义的,因此编译器可以自行选择内联调用。
在第二种情况下,编译器将为所有具有相同签名的函数实例化模板,因此它无法轻松地内联调用。
现在,聪明的编译器可能能够在编译时找出要调用的函数,特别是在这种情况下:
std::for_each(v.begin(), v.end(), &foo);

通过生成自定义实例,仍然可以将函数内联,而不是早先提到的单个通用实例。


1

我一直认为操作符()调用在运行时就像函数调用一样。它与普通函数调用有什么区别?

我的猜测是没有太大区别。为了证明这一点,可以查看编译器对每个输出的汇编代码。假设优化级别相同,它们可能几乎完全相同。(还需要传递this指针的额外细节。)

为什么内联应用程序运算符比普通函数更容易?

引用您引用的摘录:

例如,内联类的应用程序运算符比内联作为指向函数的指针传递的函数更容易。

我不是编译器专家,但我理解为:如果函数是通过函数指针调用的,则编译器很难猜测该函数指针中存储的地址是否会在运行时更改,因此用函数体替换call指令是不安全的;想想看,函数本身的主体也不一定在编译时知道。

它们如何比函数调用更快?

在许多情况下,我预计您不会注意到任何差异。但是,考虑到您引用的论点,即编译器可以自由地进行更多的内联,这可能会产生更好的代码局部性和更少的分支。如果代码经常被调用,这将产生显著的加速效果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接