STL函数式编程--为什么?

11
在C++标准模板库中,有一个名为“functional”的部分,其中许多类已经重载了它们的()运算符。
在C++中将函数作为对象使用是否会带来便利?
为什么我们不能只使用函数指针?有例子吗?

1
可能是C++ Functors - and their uses的重复问题。 - Jesse Good
4个回答

10

当然,我们可以始终使用函数指针而不是函数对象,但是函数对象提供了某些优点,例如:

  • 更好的性能:

最显著且重要的优点之一是它们更可能提供更好的性能。在函数对象的情况下,编译器可以准确地确定并内联要调用的函数,因为编译时有更多的细节可用,而在函数指针的情况下,解引用指针使得编译器难以确定实际调用的函数。

  • 函数对象是智能函数:

函数对象可以具有其他成员函数和属性,这意味着函数对象具有状态。实际上,由函数对象表示的相同函数在同一时间可能具有不同的状态。这对于普通函数是不可能的。函数对象的另一个优点是您可以在运行时初始化它们,然后再使用/调用它们。

  • 泛型编程的强大功能:

只有当函数签名不同时,普通函数才能具有不同的类型。但是,即使其签名相同,函数对象也可以具有不同的类型。实际上,由函数对象定义的每个函数行为都具有其自己的类型。这对于使用模板进行泛型编程是一个重要的改进,因为可以将函数行为作为模板参数传递。


4
为什么不能使用函数指针呢?有例子吗?
使用 C 风格的函数指针无法利用内联的优势。函数指针通常需要额外的查找间接寻址。
然而,如果重载了 operator (),编译器很容易就可以内联代码并节省额外的调用,这样会增加性能。
另一个重载 operator () 的好处是,可以设计一个隐式地将函数对象作为参数考虑进去的函数,无需将其作为单独的函数传递。手工编码越少,程序中的错误越少,可读性也更强。
Bjarne Stroustrup(C++ 创始人)的网页上的这个问题很好地解释了这一方面。
如果需要,C++ 标准(模板)库使用重载的 operator () 进行函数式编程。

有些编译器实际上会通过函数指针内联优化,当函数指针的值在编译时是常量且已知的时候,会将间接调用删除。 - Jonathan Wakely

3

> 在C++中将函数用作对象是否有任何便利之处?

是的:C++模板机制允许所有其他C/C++编程风格(C风格和OOP风格,见下文)。

> 为什么我们不能只使用函数指针?有例子吗?

但我们可以:一个简单的C函数指针也是一个具有良好定义的operator()的对象。 如果我们设计一个库,我们不想强迫任何人使用那种不需要的C指针样式。这通常像强制一切/每个人都在/使用OOP风格一样不受欢迎;见下文。


从C程序员和函数式程序员的视角来看,OOP不仅倾向于更慢,而且更啰嗦,在大多数情况下也是错误的抽象方向("信息"不应该是一个"对象")。由于这个原因,每当在其他上下文中使用"对象"这个词时,人们往往会感到困惑。

在C++中,任何具有所需属性的东西都可以被视为一个对象。在这种情况下,一个简单的C函数指针也是一个对象。这并不意味着在不需要时使用OOP范例;这只是使用模板机制的正确方式。


为了理解性能差异,比较编程(语言)风格/范例及其可能的优化:

C风格:

  • 函数指针以其闭包(OOP中的"this",指向某个结构的指针)作为第一个参数。
  • 要调用函数,需要先访问函数的地址。
  • 这是1次间接引用;无法内联。

C++(和Java)OOP风格:

  • 派生自具有虚函数的类的对象的引用。
  • 引用是第一个指针。
  • 指向虚表的指针是第二个指针。
  • 虚表中的函数指针是第三个指针。
  • 这是3次间接引用;无法内联。

C++模板风格:

  • 带有()函数的对象的副本。
  • 由于该对象的类型在编译时已知,因此没有虚表。
  • 函数的地址在编译时已知。
  • 这是0次间接引用;可以内联。

C++模板非常灵活,可以允许上述其他两种风格,并且在内联的情况下,它们甚至可以胜过...

编译的函数式语言:(排除JVM和Javascript作为目标平台,因为缺少"proper tail calls")

  • 机器寄存器中的函数指针和其闭包引用。
  • 通常不是函数“调用”,而是类似于GOTO的跳转。
  • 函数不需要堆栈,没有返回地址、参数或局部变量在堆栈上。
  • 函数具有可垃圾回收的闭包,其中包含参数和指向下一个要调用的函数的指针。
  • 为了让CPU预测跳转,函数的地址需要尽早加载到寄存器中。
  • 这是一次间接跳转,可能会进行跳转预测;几乎与内联一样快。

我不确定这如何回答OP提出的任何问题。 - Alok Save
@Als:我已经编辑了我的答案,以直接回答问题。我的大部分答案包含我认为是间接询问的信息。 - comonad

2
主要的区别在于函数对象比纯函数指针更强大,因为它们可以持有状态。大多数算法使用模板函数而不是纯函数指针,这使得可以使用强大的构造器(如绑定器(binders))来通过填充存储在函数对象上的值来调用具有不同签名的函数,或者C++11中的新特性lambda。一旦算法被设计为接受函数对象,提供一组预定义的通用函数对象就是自然而然的事情。
此外,在大多数情况下,这些函数对象都是简单的类,编译器具有完整的定义并且可以内联函数调用来提高性能。这就是为什么std::sort比来自C库的qsort快得多的原因。

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