嵌套函数在 gcc 中是一件坏事吗? [关闭]
Posted
技术标签:
【中文标题】嵌套函数在 gcc 中是一件坏事吗? [关闭]【英文标题】:Are nested functions a bad thing in gcc ? [closed] 【发布时间】:2011-02-25 03:15:41 【问题描述】:我知道嵌套函数不是标准 C 的一部分,但由于它们存在于 gcc 中(事实上 gcc 是我唯一关心的编译器),我倾向于经常使用它们。
这是一件坏事吗?如果是这样,你能告诉我一些讨厌的例子吗? gcc 中嵌套函数的状态是什么?他们会被删除吗?
【问题讨论】:
【参考方案1】:嵌套函数确实不会做任何非嵌套函数不能做的事情(这就是 C 和 C++ 都没有提供它们的原因)。您说您对其他编译器不感兴趣 - 好吧,目前这可能是真的,但谁知道未来会带来什么?我会避免使用它们以及所有其他 GCC “增强功能”。
一个小故事来说明这一点 - 我曾经在一家主要使用 DEC 盒子的英国理工学院工作 - 特别是 DEC-10 和一些 VAXen。所有的工程学院都在他们的代码中使用了 FORTRAN 的许多 DEC 扩展——他们确信我们将永远是一个 DEC 商店。然后我们用 IBM 大型机替换了 DEC-10,其 FORTRAN 编译器不支持任何扩展。我可以告诉你,那天有很多哀号和咬牙切齿。我自己的 FORTRAN 代码(一个 8080 模拟器)在几个小时内移植到 IBM(几乎全部用于学习如何驱动 IBM 编译器),因为我是用标准 FORTRAN-77 编写的。
【讨论】:
@LB 我怀疑这个实现有什么不好的——它们(就像我说的)根本没有必要。 嵌套函数可以访问声明它们的函数的变量。你如何使用非嵌套函数来管理这个? @MatthewMitchell:问题是如何将函数像参数一样传递给另一个函数。见this example:如果没有嵌套函数,你怎么能解决这个问题? 嵌套函数很有用;它们为程序员提供了匿名 lambda 的一些好处(例如,以其他函数作为参数的函数使用更少痛苦,代码重复更少),而没有分配闭包的开销。鉴于 GCC 的普遍性,使用非标准 GNU 扩展的风险与依赖于单一品牌机器的非便携式编译器中实现的功能相比是不可比的。而且,由于嵌套函数的范围是纯本地的,使用该功能的模块仍然可以链接到任何其他编译器生成的模块。 嵌套函数也很有用,因为您可以限制它的范围。例如,为什么要向文件中的所有其他函数公开另一个函数的辅助函数?在复杂情况下也减少了参数传递,因为您可以将父函数的变量用作全局变量【参考方案2】:嵌套函数有时会很有用,尤其是在使用大量变量的算法时。像写出的 4 路合并排序这样的东西可能需要保留很多局部变量,并且有许多重复的代码片段使用其中的许多。将这些重复代码位作为外部辅助例程调用将需要传递大量参数和/或让辅助例程通过另一级指针间接访问它们。
在这种情况下,我可以想象嵌套例程可能比其他编写代码的方式更有效地执行程序,至少如果编译器针对存在任何递归的情况进行优化,则通过重新调用最外层函数;在空间允许的情况下,内联函数在非缓存 CPU 上可能会更好,但通过单独的例程提供的更紧凑的代码可能会有所帮助。如果内部函数不能递归调用自己或彼此,它们可以与外部函数共享一个堆栈帧,从而能够访问其变量而不会因额外的指针取消引用而造成时间损失。
话虽如此,我会避免使用任何特定于编译器的功能,除非在直接收益超过可能因必须以其他方式重写代码而导致的任何未来成本的情况下。
【讨论】:
【参考方案3】:与大多数编程技术一样,嵌套函数应仅在适当时使用。
您不必强制使用此方面,但如果您愿意,嵌套函数可以通过直接访问包含函数的局部变量来减少传递参数的需要。这很方便。谨慎使用“不可见”参数可以提高可读性。粗心的使用会使代码更加不透明。
避免部分或全部参数使得在其他地方重用嵌套函数变得更加困难,因为任何新的包含函数都必须声明这些相同的变量。重用通常是好的,但是很多函数永远不会被重用,所以这通常没关系。
由于变量的类型与其名称一起继承,因此重用嵌套函数可以为您提供廉价的多态性,例如模板的有限和原始版本。
如果函数无意中访问或更改其容器的变量之一,使用嵌套函数还会带来错误的危险。想象一个包含对嵌套函数的调用的 for 循环,该嵌套函数包含一个使用相同索引但没有本地声明的 for 循环。如果我正在设计一种语言,我会包含嵌套函数,但需要“inherit x”或“inherit const x”声明,以使正在发生的事情更加明显,并避免意外的继承和修改。
还有其他几种用途,但嵌套函数做的最重要的事情可能是允许外部不可见的内部辅助函数,扩展 C 和 C++ 的静态非外部函数或 C++ 的私有非公共函数。有两层封装比一层要好。它还允许函数名称的局部重载,因此您不需要长名称来描述每个函数的工作类型。
当包含函数存储指向包含函数的指针以及允许多级嵌套时存在内部复杂性,但编译器编写者已经处理这些问题半个多世纪了。没有技术问题使添加到 C++ 比添加到 C 更难,但好处更少。
可移植性很重要,但 gcc 在许多环境中都可用,并且至少有一个其他编译器系列支持嵌套函数 - IBM 的 xlc 在 AIX、PowerPC 上的 Linux、BlueGene 上的 Linux、Cell 上的 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 函数,它们是真正的匿名嵌套函数。因此,编程世界有很多经验与他们赞成和反对。
嵌套函数很有用,但要小心。始终在有帮助的地方使用任何功能和工具,而不是在有伤害的地方使用。
【讨论】:
【参考方案4】:正如您所说,它们不是 C 标准的一部分,因此它们是一件坏事,因此许多(任何?)其他 C 编译器都没有实现它们。
还请记住,g++ 不实现嵌套函数,因此如果您需要获取其中的一些代码并将其转储到 C++ 程序中,则需要删除它们。
【讨论】:
这很有趣,他们提到了 C++ 的词法闭包,但没有在 g++ 中实现扩展 不幸的是,C 和 C++ 标准委员会完全不同,即使是 GCC 和 Clang 等常见的编译器工具包也使用相同的 C 和 C++ 后端【参考方案5】:嵌套函数可能很糟糕,因为在特定条件下,NX(不执行)安全位将被禁用。这些条件是:
使用 GCC 和嵌套函数
使用了指向嵌套函数的指针
嵌套函数从父函数访问变量
该架构提供 NX(不执行)位保护,例如 64 位 linux。
当满足以上条件时,GCC会创建一个蹦床https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html。为了支持蹦床,堆栈将被标记为可执行。见:https://www.win.tue.nl/~aeb/linux/hh/protection.html
禁用 NX 安全位会产生多个安全问题,其中一个值得注意的问题是禁用缓冲区溢出保护。具体来说,如果攻击者将一些代码放在堆栈上(例如作为用户可设置图像、数组或字符串的一部分),并且发生缓冲区溢出,那么攻击者的代码可能会被执行。
【讨论】:
您已经说过了,但需要重复一遍:不会生成蹦床(因此没有安全风险)除非代码采用嵌套函数的地址。对于许多编程风格来说,这是很少见的。【参考方案6】:更新
我投票删除我自己的帖子,因为它不正确。具体来说,编译器必须插入一个 trampoline 函数以利用嵌套函数,因此任何节省的堆栈空间都将丢失。
如果有编译大师想纠正我,请纠正我!
原答案:
迟到了,但我不同意接受的答案的断言
嵌套函数真的不会做任何你不能做的事情 非嵌套的。
具体来说:
TL;DR:嵌套函数可以减少嵌入式环境中的堆栈使用率
嵌套函数使您可以将词法范围的变量作为“本地”变量访问,而无需将它们推入调用堆栈。这在资源有限的系统上工作时非常有用,例如嵌入式系统。考虑这个人为的例子:
void do_something(my_obj *obj)
double times2()
return obj->value * 2.0;
double times4()
return times2() * times2();
...
请注意,一旦进入 do_something(),由于嵌套函数,对 times2() 和 times4() 的调用不需要将任何参数压入堆栈,只需返回地址(智能编译器甚至会优化尽可能把它们拿出来)。
想象一下,如果有很多内部函数需要访问的状态。如果没有嵌套函数,所有这些状态都必须在堆栈上传递给每个函数。嵌套函数让您可以像访问局部变量一样访问状态。
【讨论】:
除非有人能证明我的update
评论是错误的,否则我建议删除这个帖子,因为它完全是错误的。【参考方案7】:
我同意 Stefan 的例子,我唯一一次使用嵌套函数(然后我声明它们 inline
)是在类似的场合。
我还建议你应该很少使用嵌套的内联函数,并且在你使用它们的那几次你应该(在你的脑海和一些评论中)有一个摆脱它们的策略(甚至可能用条件来实现它) #ifdef __GCC__
编译)。
但是 GCC 是一个免费的(就像在语音中一样)编译器,它会有所不同......而且一些 GCC 扩展往往会成为事实上的标准,并由其他编译器实现。
另一个我认为非常有用的 GCC 扩展是计算的 goto,即label as values。在编码自动机或字节码解释器时非常方便。
【讨论】:
【参考方案8】:通过减少显式参数传递的数量而不引入大量全局状态,可以使用嵌套函数使程序更易于阅读和理解。
另一方面,它们不能移植到其他编译器。 (注意编译器,而不是设备。没有多少地方不运行 gcc)。
因此,如果您发现可以通过使用嵌套函数使程序更清晰的地方,您必须问自己“我是在优化可移植性还是可读性”。
【讨论】:
【参考方案9】:我只是在探索嵌套函数的不同用途。作为 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 能够优化为单个“调用”,但我想这对于非平凡的函数调用左右不起作用。在这种情况下,可以使用“缓存”,但这会增加可读性。
【讨论】:
【参考方案10】:我需要嵌套函数来允许我在对象之外使用实用程序代码。
我有负责各种硬件设备的对象。它们是通过指针作为参数传递给成员函数的结构,就像在 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 不能具有全局范围,因为存在多个实例化,并且这些实例化可能同时运行。由于访问是通过 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 可以由具有指针的东西初始化,然后从任何地方调用。虽然不理想。
【讨论】:
【参考方案11】:在任何严肃的编程语言中,嵌套函数都是必不可少的。
没有它们,功能的实际意义就无法使用。
这称为词法作用域。
【讨论】:
这意味着大多数现有语言都不适合“认真编程”!以上是关于嵌套函数在 gcc 中是一件坏事吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章