嵌套函数在gcc中是一件坏事吗?

时间:2010-05-28 13:22:06

标签: c gcc closures nested-function

我知道嵌套函数不是标准C的一部分,但由于它们存在于gcc中(并且gcc是我唯一关心的编译器),我倾向于经常使用它们。

这是件坏事吗?如果是这样,你能告诉我一些令人讨厌的例子吗? gcc中嵌套函数的状态是什么?他们会被删除吗?

11 个答案:

答案 0 :(得分:17)

嵌套函数实际上没有做任何你不能用非嵌套函数做的事情(这就是为什么C和C ++都不提供它们)。你说你对其他编译器不感兴趣 - 这一点可能是真的,但是谁知道未来会带来什么?我会避免它们,以及所有其他GCC“增强功能”。

一个小故事来说明这一点 - 我曾经为英国Polytechinc工作,主要使用DEC盒子 - 特别是DEC-10和一些VAXen。所有工程师都在他们的代码中使用FORTRAN的许多DEC扩展 - 他们确信我们将永远是DEC商店。然后我们用一台IBM大型机替换了DEC-10,它的FORTRAN编译器不支持任何扩展。我可以告诉你,当天有许多人哭泣和咬牙切齿。我自己的FORTRAN代码(一个8080模拟器)在几个小时内移植到IBM(几乎全部用于学习如何驱动IBM编译器),因为我已经用沼泽标准FORTRAN-77编写了它。

答案 1 :(得分:7)

有时候嵌套函数会很有用,尤其是对于大量变量进行混乱的算法。像写出的4路合并排序之类的东西可能需要保留很多局部变量,并且有许多重复代码使用其中许多变量。将那些重复代码作为外部辅助例程调用将需要传递大量参数和/或让辅助例程通过另一级别的指针间接访问它们。

在这种情况下,我可以想象嵌套例程可能比其他编写代码的方式允许更高效的程序执行,至少如果编译器优化了存在任何递归的情况,则通过重新调用最外面的功能;对于非缓存的CPU,内联函数(空间允许)可能更好,但通过单独的例程提供的更紧凑的代码可能会有所帮助。如果内部函数不能递归地调用它们自己或彼此,它们可以与外部函数共享一个堆栈帧,从而能够访问它的变量而不会额外指针解除引用的时间损失。

所有这些,我会避免使用任何特定于编译器的功能,除非在直接利益超过可能由于必须以其他方式重写代码而导致的任何未来成本的情况下。

答案 2 :(得分:6)

与大多数编程技术一样,嵌套函数应该在适当的时候使用。

您不必使用此方面,但如果需要,嵌套函数可以通过直接访问其包含函数的局部变量来减少传递参数的需要。那很方便。仔细使用“隐形”参数可以提高可读性。粗心使用会使代码变得更加不透明。

避免某些或所有参数使得在其他地方重用嵌套函数变得更加困难,因为任何新的包含函数都必须声明这些相同的变量。重复使用通常是好的,但许多功能永远不会被重复使用,因此通常无关紧要。

由于变量的类型与其名称一起继承,因此重用嵌套函数可以为您提供廉价的多态性,例如模板的有限和原始版本。

如果函数无意中访问或更改了其容器的某个变量,则使用嵌套函数也会带来错误的危险。想象一个for循环包含对包含for循环的嵌套函数的调用,该循环使用相同的索引而没有本地声明。如果我正在设计一种语言,我会包含嵌套函数,但需要“继承x”或“继承const x”声明,以使其更明显地发生了什么,并避免意外的继承和修改。

还有其他一些用途,但嵌套函数最重要的可能是允许外部不可见的内部辅助函数,C和C ++的静态非外部函数的扩展或C ++的私有非公共函数。具有两个封装级别优于一个封装级别。它还允许本地重载函数名称,因此您不需要长名称来描述每个函数的工作类型。

当包含函数存储指向包含函数的指针时,以及允许多层嵌套时,内部会出现并发症,但编译器编写者已经处理了这些问题超过半个世纪。没有任何技术问题使得添加到C ++比使用C更难,但好处却少了。

可移植性很重要,但gcc在许多环境中都可用,并且至少还有一个其他编译器系列支持嵌套函数 - 在AIX上可用的IBM xlc,在PowerPC上运行Linux,在BlueGene上运行Linux,在Linux上运行Linux和z / OS。看到  http://publib.boulder.ibm.com/infocenter/comphelp/v8v101index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fnested_functions.htm

嵌套函数可用于一些新的(例如Python)和更多传统语言,包括Ada,Pascal,Fortran,PL / I,PL / IX,Algol和COBOL。 C ++甚至有两个受限制的版本 - 本地类中的方法可以访问其包含函数的静态(但不是自动)变量,并且任何类中的方法都可以访问静态类数据成员和方法。即将推出的C ++标准具有lamda函数,它们实际上是匿名嵌套函数。所以编程世界有很多经验和赞成。

嵌套函数很有用但要小心。始终使用他们帮助的任何功能和工具,而不是他们受伤的地方。

答案 3 :(得分:5)

正如你所说,从某种意义上说它们不是C标准的一部分是坏事,因此很多(任何?)其他C编译器都没有实现它们。

另外请记住,g ++没有实现嵌套函数,因此如果您需要将某些代码转储并转储到C ++程序中,则需要删除它们。

答案 4 :(得分:3)

嵌套函数可能不好,因为在特定条件下,NX(无执行)安全位将被禁用。那些条件是:

  • 使用GCC和嵌套函数

  • 使用指向嵌套函数的指针

  • 嵌套函数访问父函数

  • 中的变量
  • 该架构提供NX(无执行)位保护,例如64位Linux。

当满足上述条件时,GCC将创建一个蹦床https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html。为了支持trampolines,堆栈将被标记为可执行文件。见:https://www.win.tue.nl/~aeb/linux/hh/protection.html

禁用NX安全位会产生一些安全问题,其中值得注意的是缓冲区溢出保护被禁用。具体来说,如果攻击者在堆栈上放置了一些代码(比如作为用户可设置图像,数组或字符串的一部分),并且发生了缓冲区溢出,则可以执行攻击者代码。

答案 5 :(得分:3)

晚会,但我不同意接受的答案的断言

  

嵌套功能确实无法做任何你无法做到的事情   非嵌套的。

具体做法是:

TL; DR:嵌套函数可以减少嵌入式环境中的堆栈使用

嵌套函数使您可以访问词法范围的变量作为" local"变量而不需要将它们推送到调用堆栈。当在具有有限资源的系统上工作时,这可能非常有用,例如,嵌入式系统。考虑这个人为的例子:

void do_something(my_obj *obj) {
    double times2() {
        return obj->value * 2.0;
    }
    double times4() {
        return times2() * times2();
    }
    ...
}

请注意,一旦你进入do_something(),由于嵌套函数,对times2()和times4()的调用不需要将任何参数推送到堆栈上,只需返回地址(和智能编译器甚至可以在可能的情况下优化它们。)

想象一下,如果有很多状态需要访问内部函数。没有嵌套函数,所有状态都必须在堆栈上传递给每个函数。嵌套函数允许您像局部变量一样访问状态。

答案 6 :(得分:2)

我同意Stefan的例子,并且我唯一一次使用嵌套函数(然后我宣布它们为inline)也是在类似的场合。

我还建议您很少使用嵌套的内联函数,并且几次使用它们时,您应该(在您的脑海中和某些注释中)有一个策略来摆脱它们(甚至可能用条件实现它) #ifdef __GCC__汇编)。

但GCC是一个免费(如语音)编译器,它会产生一些不同......而且一些GCC扩展往往成为事实上的标准,并由其他编译器实现。

我认为另一个GCC扩展非常有用的是计算goto,即label as values。编码自动机或字节码解释器时非常方便。

答案 7 :(得分:1)

通过减少显式参数传递的数量而不引入大量全局状态,可以使用嵌套函数使程序更易于阅读和理解。

另一方面,它们不能移植到其他编译器。 (注意编译器,而不是设备.Gcc没有运行的地方不多)。

因此,如果你看到一个可以通过使用嵌套函数使程序更清晰的地方,你必须问自己'我是否在优化便携性或可读性'。

答案 8 :(得分:0)

我只是在探索一些不同类型的嵌套函数。作为C语言中“懒惰评估”的一种方法。

想象一下这样的代码:

void vars()
{
  bool b0 = code0; // do something expensive or to ugly to put into if statement
  bool b1 = code1;

  if      (b0) do_something0();
  else if (b1) do_something1();
} 

void funcs()
{
  bool b0() { return code0; }
  bool b1() { return code1; }

  if      (b0()) do_something0();
  else if (b1()) do_something1();
}

通过这种方式,您可以获得清晰度(当您第一次看到此类代码时可能会有点混乱),而代码仍在执行时以及仅在需要时执行。 同时将它转换回原始版本非常简单。

如果多次使用相同的“值”,则会出现一个问题。当所有值在编译时都已知时,GCC能够优化为单个'call',但我想这对于非平凡的函数调用是不行的。在这种情况下,可以使用“缓存”,但这会增加不可读性。

答案 9 :(得分:0)

我需要嵌套函数,以允许我在对象外部使用实用程序代码。

我有照顾各种硬件设备的对象。它们是通过指针作为成员函数的参数传递的结构,而不是在c ++中自动发生的结构。

所以我可能有

    static int ThisDeviceTestBram( ThisDeviceType *pdev )
    {
        int read( int addr ) { return( ThisDevice->read( pdev, addr ); }
        void write( int addr, int data ) ( ThisDevice->write( pdev, addr, data ); }
        GenericTestBram( read, write, pdev->BramSize( pdev ) );
    }

GenericTestBram也不知道有关具有多个实例化的ThisDevice。但是它所需要的只是阅读和书写的方式以及大小。 ThisDevice-> read(...)和ThisDevice-> Write(...)需要指向ThisDeviceType的指针才能获取有关如何读取和写入此特定实例的块存储器(Bram)的信息。指针pdev不能具有全局scobe,因为存在多个实例化,并且这些实例化可能同时运行。由于访问是通过FPGA接口进行的,因此传递地址不是一个简单的问题,并且因设备而异。

GenericTestBram代码是一个实用函数:

    int GenericTestBram( int ( * read )( int addr ), void ( * write )( int addr, int data ), int size )
    {
        // Do the test
    }

因此,只需编写一次测试代码,而无需知道调用设备的结构细节。

即使是GCC,也无法执行此操作。问题是超出范围的指针,这是需要解决的问题。我知道唯一使f(x,...)隐式知道其父级的方法是传递值超出范围的参数:

     static int f( int x ) 
     {
         static ThisType *p = NULL;
         if ( x < 0 ) {
             p = ( ThisType* -x );
         }
         else
         {
             return( p->field );
         }
    }
    return( whatever );

函数f可以由具有指针的东西初始化,然后从任何地方调用。不过并不理想。

答案 10 :(得分:-3)

嵌套函数在任何严肃的编程语言中都是必须的。

没有它们,实际的功能感是不可用的。

它被称为词汇范围。