14

这样做的关键原因是 for_each () 实际上并没有假设 它的第三个参数是一个函数。它只是假设它的第三个参数是可以用适当的参数调用的东西。一个适当定义的对象和一个函数一样——而且通常比一个函数更好。例如,内联类的应用程序运算符比内联作为函数指针传递的函数更容易。因此,函数对象通常比普通函数执行得更快。具有应用运算符(第 11.9 节)的类的对象称为类函数对象、仿函数或简称为函数对象。

[Stroustrup,C++ 第 3 版,18.4-最后一段]

  1. 我一直认为operator()调用就像在运行时调用函数一样。它与普通的函数调用有何不同?

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

  3. 它们比函数调用快多少?

4

3 回答 3

11

一般来说,仿函数被传递给模板函数——如果你这样做,那么传递一个“真实”函数(即函数指针)或仿函数(即具有重载的类)并不重要operator()。本质上,两者都有一个函数调用运算符,因此是编译器可以实例化for_each模板的有效模板参数。 这意味着要么使用传递的函子的特定类型for_each进行实例化,要么使用传递的函数指针的特定类型进行实例化。 正是在这种专业化中,仿函数有可能胜过函数指针。

毕竟,如果你要传递一个函数指针,那么参数的编译类型就是一个函数指针。如果for_each它本身没有被内联,那么这个特定for_each的实例被编译为调用一个不透明的函数指针——毕竟,编译器怎么能内联一个函数指针呢?它只知道它的类型,而不是实际传递了该类型的哪个函数——至少,除非它可以在优化时使用非本地信息,这更难做到。

但是,如果您传递一个仿函数,那么该仿函数的编译时类型将用于实例化for_each模板。这样做时,您可能会传递一个简单的非虚拟类,其中只有一个适当的operator(). 因此,当编译器遇到对它的调用时,operator()它确切地知道是哪个实现 - 该函子的唯一实现 - 现在它可以内联它。

如果你的函子使用虚方法,潜在的优势就消失了。当然,函子是一个类,你可以用它做各种其他低效的事情。但是对于基本情况,这就是编译器优化和内联仿函数调用比函数指针调用更容易的原因。

概括

函数指针不能被内联,因为编译时编译for_each器只有函数的类型而不是函数的标识。相比之下,仿函数可以内联,因为即使编译器只有仿函数的类型,但类型通常足以唯一标识仿函数的operator()方法。

于 2010-10-14T23:26:25.393 回答
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);

并且仍然通过生成自定义实例而不是前面提到的单个通用实例来内联函数。

于 2010-10-14T23:26:40.080 回答
1

我一直认为 operator() 调用就像在运行时调用函数一样。它与普通的函数调用有何不同?

我的猜测不是很多。为了证明这一点,请查看每个编译器的汇编输出。假设优化水平相同,它可能几乎相同。this(带有指针必须通过的附加细节。)

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

引用你引用的简介:

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

我不是编译器人员,但我将其解读为:如果函数是通过函数指针调用的,编译器很难猜测存储在该函数指针中的地址是否会在运行时改变,因此不是安全地用call函数体替换指令;想一想,函数体本身在编译时不一定是已知的。

它们比函数调用快多少?

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

于 2010-10-14T23:55:14.237 回答