【问题标题】:Why are nested functions not supported by the C standard?为什么 C 标准不支持嵌套函数?
【发布时间】:2009-08-28 16:25:43
【问题描述】:

在汇编中实现似乎并不太难。

gcc 还有一个标志(-fnested-functions)来启用它们。

【问题讨论】:

  • 实施/不会实施“太难”与标准定义机构选择在标准中包含或省略的内容之间存在差异。 GCC对此提供支持仅仅意味着它是在标准范围之外实现的——也就是说,它是一个非标准特性。 GCC 只是一个符合 C 标准的编译器的实现;它不限于仅提供标准中的内容。
  • @Matt:听起来更像是一个答案而不是评论
  • Matt Ball:我的问题确实是为什么标准定义机构选择省略这样的功能。我对 gcc 的引用更多地是“可以做到,为什么不可以”的一个例子。
  • @des4maisons “它可以做到,为什么不这样做”是你最终使用 C++ 等语言的方式。这是好事还是坏事取决于你。 (是的,C++ 也不直接支持嵌套函数,但你总是可以创建一个本地函子)。

标签: c function standards


【解决方案1】:

事实证明,它们实际上并不是那么容易正确实施。

内部函数应该可以访问包含范围的变量吗? 如果没有,嵌套它就没有意义了;只需将其设为静态(以限制对其所在的翻译单元的可见性)并添加注释“这是仅由 myfunc() 使用的辅助函数”。

但是,如果您想访问包含范围的变量,那么您基本上是在强制它生成闭包(替代方法是限制您可以对嵌套函数执行的操作,以使其无用)。 我认为 GCC 实际上通过为包含函数的每次调用生成(在运行时)一个唯一的 thunk 来处理这个问题,它设置一个上下文指针,然后调用嵌套函数。这最终是一个相当令人讨厌的黑客,以及一些完全合理的实现无法做到的事情(例如,在一个禁止执行可写内存的系统上 - 许多现代操作系统出于安全原因这样做)。 使其正常工作的唯一合理方法是强制所有函数指针携带隐藏的上下文参数,并且所有函数都接受它(因为在一般情况下,您不知道何时调用它是闭包还是一个未封闭的函数)。由于技术和文化原因,这在 C 中是不合适的,因此我们只能选择使用显式上下文指针来伪造闭包而不是嵌套函数,或者使用具有所需基础设施的高级语言正确地做。

【讨论】:

  • 你可以禁止获取嵌套函数的地址,不是吗?
  • 在嵌套函数中实现上层变量访问的两种最常见的方式是“静态链接”,其中每个函数都有一个指向其词法父级的堆栈帧的指针,以及“显示”,它由一组这样的指针组成。它基本上是一个链表与一个固定数组。除非您希望允许在其父级退出后调用内部函数(例如,Pascal 不允许这样做),否则您不需要完整的闭包。 en.wikipedia.org/wiki/Call_stack#Lexically_nested_routines
  • 我曾经在 Mac 上使用 Pascal(可能是 Think Pascal)从事过一个项目,有人确实使用了指向嵌套函数的指针并调用它们导致了严重的崩溃,直到他们陷入了一个虚假的额外参数(我忘了如何;这必须通过类型检查)。我最终发现嵌套函数包含静态链接作为额外参数。
  • 更多:他们侥幸逃脱,因为他们没有使用通过静态链接访问的变量(函数全局,其父函数本地)。我试图向首席开发人员解释这一点,但他不感兴趣:“它只是有效,所以我为什么要关心?”该项目失败了。我很惊讶吗?
【解决方案2】:

我想来自 BDFL (Guido van Rossum) 的 quote something

这是因为嵌套函数定义无权访问 周围块的局部变量——只对全局变量 包含模块。这样做是为了不查找全局变量 必须遍历一系列字典——在 C 中,只有两个 嵌套范围:本地和全局(除此之外,内置)。 因此,嵌套函数的用途有限。这是一个 深思熟虑的决定,基于语言经验,允许 任意嵌套,例如 Pascal 和两个 Algols - 也使用代码 许多嵌套范围的可读性与具有太多 GOTO 的代码差不多。

重点是我的。

我相信他指的是 Python 中的嵌套范围(正如 David 在 cmets 中指出的那样,这是从 1993 年开始的,Python 现在确实支持完全嵌套的函数)——但我认为该声明仍然适用。

它的另一部分可能是closures

如果你有类似 C 代码的函数:

(*int()) foo() {
    int x = 5;
    int bar() {
        x = x + 1;
        return x;
    }
    return &bar;
}

如果您在某种回调中使用bar,x 会发生什么?这在许多更新的、更高级的语言中都有很好的定义,但是 AFAIK 没有明确定义的方法来跟踪 C 中的 x - bar 每次都返回 6,还是连续调用 bar 返回递增值?这可能会给 C 相对简单的定义增加一层全新的复杂性。

【讨论】:

  • 假设,如果您使用 bar 作为回调,您将在包含它的函数返回后使用局部变量 (bar) 的地址。如果 bar 是一个 int(比如),这将是正确的,应该是未定义的。
  • 但是大多数具有一阶函数的语言(例如 Python 和 Lua)确实以经典的闭包风格定义了这种行为——bar 将返回 5,6,7,... 嵌套函数几乎要求函数是一阶值。
  • 确实如此……嗯,我没有考虑过。但是 C 和 Python 在返回“本地”数组(或 Python 中的列表)时也会做不同的事情。 C 将返回一个指向不再明确定义的堆栈内存的指针; Python 将返回一个全新的列表。所以它们是不同的范式,我不确定它们是否具有可比性。
  • @Mark 不,他们不支持 - Pascal 一直支持他们。
  • 我只想指出,有问题的引用已有 10 多年的历史了:python.org/search/hypermail/python-1993/0343.html 而且 Python 完全支持嵌套函数和嵌套范围。
【解决方案3】:

有关潜在问题,请参阅 C FAQ 20.24the GCC manual

如果你尝试调用嵌套函数 通过其地址后 包含函数已退出,所有 地狱将挣脱。如果你尝试 在包含范围级别之后调用它 已经退出,如果它指的是一些 不再存在的变量 范围,您可能很幸运,但事实并非如此 冒险是明智的。然而,如果, 嵌套函数不引用 任何超出范围的东西, 你应该是安全的。

这并不比 C 标准的其他一些有问题的部分更严重,所以我想说原因主要是历史原因(C99 与 K&R C 的功能方面并没有真正的不同 )。

在某些情况下,具有词法范围的嵌套函数可能很有用(考虑一个递归内部函数,它不需要为外部范围内的变量提供额外的堆栈空间,而无需静态变量),但希望你能相信编译器会正确内联此类函数,即具有单独函数的解决方案只会更加冗长。

【讨论】:

  • 正是递归可能是他们不允许嵌套函数的主要原因。这要么意味着它们只需要尾递归(因此堆栈帧不会改变),要么它们确实必须支持闭包之类的东西。
【解决方案4】:

嵌套函数是一件非常微妙的事情。你会让他们关闭吗?如果不是,那么它们对常规函数没有优势,因为它们无法访问任何局部变量。如果他们这样做了,那么你对堆栈分配的变量做了什么?您必须将它们放在其他地方,这样如果您稍后调用嵌套函数,变量仍然存在。这意味着它们会占用内存,因此您必须在堆上为它们分配空间。没有 GC,这意味着程序员现在负责清理函数。等等... C# 可以做到这一点,但他们有一个 GC,而且它是一种比 C 新得多的语言。

【讨论】:

  • 如果我们将堆栈分配的变量设为闭包,会有什么问题?
  • 另外,这听起来可能很愚蠢,但是为什么你需要带有嵌套函数的闭包呢?嵌套函数有什么特别之处?谢谢!
  • @Lazer:堆栈分配的变量在函数返回时消失。如果你有一个闭包,那么即使在它返回之后,该函数也必须能够访问这些变量,以防它再次被调用。所以你必须把它们放在堆栈之外的某个地方。为什么你需要它们?询问使用 JavaScript、Python、Ruby、Scheme、LISP、C# 等的人。它们对于创建事件处理程序等某些事情很方便。在某些情况下还能让你的代码更优雅。
  • @Claudiu:词法嵌套函数只能在其父函数处于活动状态时处于活动状态。嵌套函数不需要在任何函数返回后将局部变量保存在内存中。 (除非一种语言允许保存嵌套函数的地址并在父函数完成后间接调用它,但我希望这是一个致命错误或未定义的行为。)
  • @KeithThompson:取决于实现。在 Python 中,它们绝对可以在父级返回之后处于活动状态。这既不是致命错误也不是未定义,而是定义明确且非常方便。我想你可以实现它们,使它们不存储任何内存,因此不应该从父级中返回。我想这仍然有一些用处
【解决方案5】:

将成员函数添加到结构中也不会太难,但它们也不在标准中。

功能不会单独添加到 C 标准中,无论它们是否易于实现。它是许多其他因素的组合,包括编写标准的时间点以及当时常见/实用的内容。

【讨论】:

  • 嗯......没有收到明确的“这就是为什么”的答案,也许你是对的。不过,我仍然希望有一个很好的理由。
  • 不幸的是,原因可能很简单,就像“没有足够的人关心这个功能”。 C 标准过程通常是编纂现有实践的过程之一。由于很少有编译器这样做过,因此从未真正推动它成为标准。鉴于它在 C 中没有多大帮助(因为你没有闭包或嵌套全局范围之类的东西,这会让嵌套函数变得非常有趣),它只是从来没有进入过那里。
  • 规范化必须为章程规范现有实践。他们做的不止这些,但添加嵌套函数会被认为走得很远。您必须回到 70 年代,将 C 定义为系统语言——与 BCPL 和 BLISS 以及汇编程序相提并论。
【解决方案6】:

ANSIC成立20年。也许在 1983 年到 1989 年之间,委员会可能已经根据当时的编译器技术状况进行了讨论,但如果他们这样做了,他们的推理就会在模糊而遥远的过去中消失。

【讨论】:

  • 很确定你的意思是在那里几年......呵呵。
  • :) 是的,这不是我第一次混了几十年,编码了 30 年,所以我倾向于用几十年来思考,然后把它们弄糊涂,这就是年龄对你的影响. :(
  • 我怀疑这是一个最先进的问题。这个问题从 60 年代就解决了。
  • 我怀疑它比这更简单。 C 委员会并不以编造东西而闻名(与 C++ 委员会相反,他们定期设计新事物)。 C 委员会喜欢将人们已经在做的事情标准化,而很少有编译器会这样做。所以他们还没有标准化。如果它更常用,他们可能会添加它。
【解决方案7】:

还有一个原因:嵌套函数是否有价值并不清楚。二十多年前,我曾经在 (VAX) Pascal 中进行大规模编程和维护。我们有很多旧代码大量使用嵌套函数。起初,我认为这很酷(与我之前工作过的 K&R C 相比)并开始自己做。过了一会儿,我认为这是一场灾难,并停止了。

问题是一个函数在作用域内可能有大量许多变量,计算它嵌套在其中的所有函数的变量。 (一些旧代码有十级嵌套;五级很常见,在我改变主意之前,我自己编写了一些后者。)嵌套堆栈中的变量可以具有相同的名称,因此“内部”函数局部变量可以在更多“外部”函数中屏蔽同名变量。函数的局部变量,在类 C 语言中是完全私有的,可以通过调用嵌套函数来修改。这种爵士乐的可能组合几乎是无限的,是阅读代码时难以理解的噩梦。

所以,我开始将这个编程构造称为“半全局变量”而不是“嵌套函数”,并告诉其他编写代码的人唯一比全局变量更糟糕的是半全局变量,请不要再创造了。如果可以的话,我会从语言中禁止它。可悲的是,编译器没有这样的选项......

【讨论】:

    【解决方案8】:

    我不同意 Dave Vandervies。

    定义一个嵌套函数比在全局范围内定义它、使其成为静态并添加注释说“这是一个仅由 myfunc() 使用的辅助函数”更好的编码风格。

    如果您需要此辅助函数的辅助函数怎么办?您是否会添加注释“这是仅由 myfunc 使用的第一个辅助函数的辅助函数”?在不完全污染命名空间的情况下,您从哪里获取所有这些功能所需的名称?

    代码写得有多混乱?

    当然,如何处理闭包存在问题,即返回一个指向函数的指针,该函数可以访问在返回它的函数中定义的变量。

    【讨论】:

      【解决方案9】:

      要么您不允许在包含函数中引用包含函数的局部变量,并且嵌套只是一个没有太多用处的作用域功能,要么您允许。如果你这样做了,这不是一个那么简单的功能:你必须能够在访问正确数据的同时从另一个函数调用嵌套函数,并且还必须考虑递归调用。这不是不可能的——技术是众所周知的,并且在设计 C 时已经很好地掌握了(Algol 60 已经具有该功能)。但是它使运行时组织和编译器复杂化,并阻止了到汇编语言的简单映射(函数指针必须携带有关它的信息;还有其他选择,例如使用 gcc)。它超出了 C 设计的系统实现语言的范围。

      【讨论】:

        猜你喜欢
        • 2014-03-13
        • 1970-01-01
        • 1970-01-01
        • 2013-09-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-06
        • 2013-08-11
        • 1970-01-01
        相关资源
        最近更新 更多