【问题标题】:The Benefits of Using Function Pointers使用函数指针的好处
【发布时间】:2010-10-14 14:56:39
【问题描述】:

我已经编程了几年,并且在某些情况下使用过函数指针。我想知道什么时候出于性能原因使用它们合适或不合适,我的意思是在游戏而不是商业软件的背景下。

函数指针很快,John Carmack 在 Quake 和 Doom 源代码中滥用了它们,因为他是个天才 :)

我想更多地使用函数指针,但我想在最合适的地方使用它们。

如今,在 C、C++、C# 和 Java 等现代 c 风格语言中,函数指针的最佳和最实用的用途是什么?

【问题讨论】:

  • 在 JavaScript 中编写一些函数是一流的代码,你会爱上它。只是一个小例子:我有一个类可以监视带有更改设置的文件。当你创建这个类时,你给它传递了一个委托,以便在每次文件更改时调用。

标签: c# c++ c programming-languages function-pointers


【解决方案1】:

在 C# 中使用事件处理程序或委托时,实际上是在使用函数指针。

不,它们与速度无关。函数指针是为了方便。

乔纳森

【讨论】:

    【解决方案2】:

    函数指针在很多情况下被用作回调。一种用途是作为排序算法中的比较函数。因此,如果您尝试比较自定义对象,您可以提供一个函数指针,指向知道如何处理该数据的比较函数。

    也就是说,我将提供我从前任教授那里得到的一句话:

    对待一个新的 C++ 特性,就像对待一个拥挤的房间里的自动武器一样:永远不要仅仅因为它看起来很漂亮而使用它。等你明白后果,不要装可爱,写你知道的,知道你写的。

    【讨论】:

    • 哈哈,原来如此!让我想起了gotw.ca/publications/advice97.htm :)
    • 这正是他所引用的!太棒了。
    • “写下你所知道的”如果你独自完成这个项目就很有效。但是如果你擅长 **arrays 并且不熟悉 STL,那么在一个团队中工作并不酷。与大项目中代码周围的纯 C 类函数相同。
    【解决方案3】:

    只是说 C#,但函数指针在 C# 中使用。委托和事件(以及 Lambda 等)都是底层的函数指针,因此几乎所有 C# 项目都会充斥着函数指针。基本上每个事件处理程序、每个 LINQ 查询附近等 - 都将使用函数指针。

    【讨论】:

      【解决方案4】:

      函数指针很快

      在什么情况下?相比?

      听起来你只是想使用函数指针来使用它们。那会很糟糕。

      指向函数的指针通常用作回调或事件处理程序。

      【讨论】:

      • 快速,因为它们实际上是跳转到代码的另一部分,从而使您免于返回当前方法并可能离开循环然后稍后返回。
      • 我明白你的意思并同意,不应该仅仅为了它而使用它们。
      • 常规函数调用也是“跳转到代码的另一部分”。是的,这是有用的行为,但您通常通过简单的函数调用来获得它,不需要涉及函数指针
      【解决方案5】:

      如今,在现代 c 风格语言中,整数的最佳和最实用的用途是什么?

      【讨论】:

      • 我道歉,但我不遵守?
      • 如果需要整数,请使用整数。如果需要函数指针,请使用函数指针。两者(像所有编程结构一样)都只是工具,在任何方面都不是特别快或神奇。
      • 我喜欢你的讽刺,先生。
      • 拍拍自己的后背高尔夫拍手
      【解决方案6】:

      如果您直到运行时才知道目标平台支持的功能(例如 CPU 功能、可用内存),它们会很有用。显而易见的解决方案是编写如下函数:

      int MyFunc()
      {
        if(SomeFunctionalityCheck())
        {
          ...
        }
        else
        {
          ...
        }
      }
      

      如果这个函数在重要循环的深处被调用,那么最好为 MyFunc 使用函数指针:

      int (*MyFunc)() = MyFunc_Default;
      
      int MyFunc_SomeFunctionality()
      {
        // if(SomeFunctionalityCheck())
        ..
      }
      
      int MyFunc_Default()
      {
        // else
        ...
      }
      
      int MyFuncInit()
      {
        if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality;
      }
      

      当然还有其他用途,例如callback functions,从内存执行字节码或创建解释语言。

      在 Windows 上执行 Intel compatible byte code,这可能对解释器有用。例如,这是一个 stdcall 函数,返回 42 (0x2A) 存储在一个可以执行的数组中:

      code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
      // mov eax, 42
      code[0] = 0x8b;
      code[1] = 0x2a;
      code[2] = 0x00;
      code[3] = 0x00;
      code[4] = 0x00;
      // ret
      code[5] = 0xc3;
      // this line executes the code in the byte array
      reinterpret_cast<unsigned int (_stdcall *)()>(code)();
      
      ...
      
      VirtualFree(code, 6, MEM_RELEASE);
      

      );

      【讨论】:

      • 应该有策略或其他类似装饰器的设计模式。当然,如果我们谈论 C++。
      【解决方案7】:

      函数指针没有什么特别“快”的。它们允许您调用在运行时指定的函数。但是您的开销与从任何其他函数调用中获得的开销完全相同(加上额外的指针间接)。此外,由于要调用的函数是在运行时确定的,因此编译器通常不能像在其他任何地方那样内联函数调用。因此,在某些情况下,函数指针加起来可能比常规函数调用慢得多。

      函数指针与性能无关,绝不应用于获得性能。

      相反,它们是对函数式编程范式的轻微认可,因为它们允许您将函数作为参数传递或在另一个函数中返回值。

      一个简单的例子是一个通用的排序函数。它必须有某种方法来比较两个元素,以确定它们应该如何排序。这可能是传递给排序函数的函数指针,实际上 c++ 的std::sort() 可以完全这样使用。如果您要求它对未定义小于运算符的类型的序列进行排序,则必须传入一个函数指针,它可以调用它来执行比较。

      这很好地引导我们找到一个更好的选择。在 C++ 中,您不仅限于函数指针。您经常使用函子代替 - 即重载运算符 () 的类,以便它们可以像函数一样被“调用”。与函数指针相比​​,函子有两个很大的优势:

      • 它们提供了更大的灵活性:它们是成熟的类,具有构造函数、析构函数和成员变量。它们可以维护状态,并且可以公开周围代码可以调用的其他成员函数。
      • 它们更快:不像函数指针,它的类型只编码函数的签名(void (*)(int) 类型的变量可能是 any 函数,它接受一个 int 并返回 void。我们可以'不知道是哪一个),函子的类型对应该调用的精确函数进行编码(由于函子是一个类,称之为 C,我们知道要调用的函数是,并且将永远是,C::operator())。这意味着编译器可以内联函数调用。这就是使通用std::sort 与专为您的数据类型设计的手动编码排序函数一样快的魔力。编译器可以消除调用用户定义函数的所有开销。
      • 它们更安全:函数指针中几乎没有类型安全性。你不能保证它指向一个有效的函数。它可能是 NULL。指针的大多数问题也适用于函数指针。它们很危险且容易出错。

      函数指针(在 C 中)或函子(在 C++ 中)或委托(在 C# 中)都解决了相同的问题,具有不同级别的优雅和灵活性:它们允许您将函数视为一等值,并传递它们就像任何其他变量一样。您可以将一个函数传递给另一个函数,它会在指定时间调用您的函数(当计时器到期时,当窗口需要重绘时,或者当它需要比较数组中的两个元素时)

      据我所知(我可能是错的,因为我已经很久没有使用 Java 了),Java 没有直接的等价物。相反,您必须创建一个实现接口并定义函数的类(例如,将其称为Execute())。然后调用foo.Execute(),而不是调用用户提供的函数(以函数指针、函子或委托的形式)。原则上类似于 C++ 实现,但没有 C++ 模板的通用性,也没有允许您以相同方式处理函数指针和函子的函数语法。

      这就是你使用函数指针的地方:当更复杂的替代方案不可用时(即你被困在 C 语言中),你需要将一个函数传递给另一个函数。最常见的场景是回调。您定义了一个函数 F,您希望系统在 X 发生时调用该函数。因此,您创建一个指向 F 的函数指针,并将其传递给相关系统。

      真的,忘记 John Carmack 吧,不要假设你在他的代码中看到的任何东西都会神奇地让你的代码变得更好,如果你复制它。他使用函数指针是因为你提到的游戏是用 C 语言编写的,没有更好的替代品,而不是因为它们是某种神奇的成分,它的存在会使代码运行得更快。

      【讨论】:

      • "函数指针与性能无关,永远不应该用来获得性能。"实际上,它们可用于提高性能,仿函数/委托等也可以。请参阅我的答案中的示例...为什么永远不要这样做?我看不出它有什么不好。
      • 是的。我的观点很简单,仅仅调用函数指针而不是调用函数并不快,因为这似乎是 OP 所暗示的。没错,如果你用它来改变程序的逻辑,它也会影响性能。
      • 在我从我的回答中获得了一些代表提升之后......重新阅读让我思考,实际上性能确实取决于函数调用机制,有时会很大 - 例如在 C++ 中,虚函数调用和虚继承添加了额外的间接层,以至于您可以想象污染整个缓存。尽管有许多其他原因,那是糟糕的代码......函数指针可以实现相同的功能(复杂继承),具有额外的内存开销,但只留下动态调用,例如每次只访问指令和数据缓存一次,而不是 2..n 次
      • @jheriko 哦,当然,不同的调用机制有不同的性能特征。我在回答中的意思是函数指针不会使函数更快,正如 OP 似乎相信的那样。相反,额外的间接和相关的缓存污染使它比直接调用慢
      • 您可以为班级名称选择不同的字母。只是说;)
      【解决方案8】:

      在 C++ 之前的黑暗时代,我在代码中使用了一种常见模式,即定义一个带有一组函数指针的结构,这些函数指针(通常)以某种方式对该结构进行操作,并为它。在 C++ 术语中,我只是在构建一个 vtable。不同之处在于我可以在运行时对结构产生副作用,以根据需要动态更改单个对象的行为。这以稳定性和易于调试为代价提供了更丰富的继承模型。然而,最大的代价是只有一个人可以有效地编写这段代码:我。

      我在 UI 框架中大量使用了它,它让我可以即时更改对象的绘制方式、命令的目标对象等等——这是很少有 UI 提供的。

      用 OO 语言形式化这个过程在各个方面都更好。

      【讨论】:

      • 没错。我使用了几种语言的代码引用来模拟 OO。仅供参考:并非所有语言都“关闭”课程。 Ruby、Javascript 和 Objective C 让您可以按照您所说的在运行时更改实现。
      【解决方案9】:

      函数指针是穷人的函数式尝试。您甚至可以提出一个论点,即拥有函数指针会使语言功能化,因为您可以使用它们编写更高阶的函数。

      没有闭包和简单的语法,它们有点恶心。所以你倾向于使用它们远远低于预期。主要用于“回调”功能。

      有时,OO 设计通过创建一个完整的接口类型来传递所需的函数来解决使用函数的问题。

      C# 有闭包,因此函数指针(它实际上存储一个对象,因此它不仅是一个原始函数,而且也是一个类型化的状态)在那里更有用。

      编辑 其中一位 cmets 表示应该演示带有函数指针的高阶函数。任何采用回调函数的函数都是高阶函数。比如,EnumWindows:

      BOOL EnumWindows(          
          WNDENUMPROC lpEnumFunc,
          LPARAM lParam
      );
      

      第一个参数是要传入的函数,很简单。但是由于 C 中没有闭包,我们得到了这个可爱的第二个参数:“指定要传递给回调函数的应用程序定义的值。”该应用定义的值允许您手动传递无类型状态以弥补闭包的不足。

      .NET 框架也充满了类似的设计。例如,IAsyncResult.AsyncState:“获取一个用户定义的对象,该对象限定或包含有关异步操作的信息。”由于 IAR 是您在回调中获得的全部内容,没有闭包,您需要一种方法将一些数据推送到异步操作中,以便稍后将其丢弃。

      【讨论】:

      • 我很好奇为什么有人将这个投票否决为-1。似乎是一个正确(并且经过深思熟虑)的答案。然后从这里 +1 以进行补偿。
      • 我没有投反对票,但也许他应该演示如何用 fps 构造高阶函数?
      【解决方案10】:

      有些场合使用函数指针可以加快处理速度。可以使用简单的调度表来代替长的 switch 语句或 if-then-else 序列。

      【讨论】:

        【解决方案11】:

        根据我的个人经验,它们可以帮助您节省大量代码。

        考虑条件:

        switch(sample_var)
        {
        
        case 0:
                  func1(<parameters>);
                  break;
        
        case 1:
                  func2(<parameters>);
                  break;
        
        
        
        up to case n:
                  funcn(<parameters>);
                  break;
        
        }
        

        其中func1() ... funcn() 是具有相同原型的函数。 我们能做的是: 声明一个包含函数地址的函数指针数组arrFuncPoint func1()funcn()

        然后整个开关盒将被替换为

        *arrFuncPoint[sample_var];

        【讨论】:

          猜你喜欢
          • 2012-05-22
          • 2017-08-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-08-15
          相关资源
          最近更新 更多