GCC 嵌套函数实现(与 C++ Lambda 对比)
Implementation of GCC's Nested Functions (vs. C++ Lambdas)

原始链接: https://uecker.codeberg.page/2026-09-05.html

本文解释了 GCC 如何实现嵌套函数,特别是那些访问父作用域变量的函数。 GCC 没有使用(旧语言中常见的)栈帧指针,而是在早期的中间端阶段对嵌套函数进行降级处理。它将所有被访问的父变量打包到一个“帧”结构中,并将指向该结构的指针作为隐藏参数传递给嵌套函数。这种方法使该特性与编译器特定的栈管理解耦,并允许标准优化器像处理其他数据结构一样处理该帧。 作者指出,虽然 C++ Lambda 表达式和 GCC 嵌套函数在语法上有表面的差异,且 C++ 使用了独特的匿名“伏地魔”(Voldemort)类型,但它们的核心实现却极为相似。两者都将嵌套代码转换为持有捕获变量的对象或结构。一个关键的技术区别在于,GCC 可能会将所有嵌套函数聚合到一个共享帧中,而 C++ 通常为每个 Lambda 创建单独的对象。最终,作者总结道,GCC 嵌套函数在语义上是 C++ Lambda 的一个子集,现代编译器完全可以使用相同的基础设施轻松支持两者。

这篇 Hacker News 讨论批评了一篇主张在 C 标准中引入 GCC 风格嵌套函数的文章。评论者指出,作者忽视了重大的技术挑战,尤其是将嵌套函数作为指针传递时所需的“蹦床(trampolines)”机制——这种机制通常需要可执行栈或特定的 ABI 支持,从而带来了安全性和可移植性风险。 相比之下,参与者指出 C++ 的 Lambda 表达式通过将闭包视为对象而非原生指针,更有效地处理了这些问题。D 语言开发者 Walter Bright 强调了 D 语言如何通过“委托(delegates)”避免这些问题;委托将函数指针与指向外围作用域的静态链接配对,创建了一种类似于 C++ 成员函数、且与 ABI 兼容的结构。 总体而言,人们普遍认为,虽然嵌套函数提供了语法上的便利,但它们引入了复杂的语义和优化障碍。现代编译器设计倾向于采用基于闭包的模型,因为它们避免了 GCC 实现中固有的栈操作陷阱;这些陷阱会迫使变量溢出到内存而非保留在寄存器中,从而对性能产生负面影响。
相关文章

原文

Implementation of GCC's Nested Functions (vs. C++ Lambdas)

Martin Uecker, 2026-09-05

Introduction

Here, I want to explain how GCC's nested function are implemented. I am not going to discuss taking the address of a nested function that may require the creation of a trampoline. We discussed this topic - and how to get around it - already in several previous blog posts. Instead, I want to describe the basic mechanism that is used to access variables of a parent function.

Nested Functions

Let us start with a very simple example.


	int foo(int k)
	{
		int bar(int x) { return x + 1; }
		return bar(k);
	}
	

Here, the nested function does not access any variable of the parent function. In this case, it can simply be lifted out of the parent function and be compiled as a separate function. Such functions can still can be useful to define small helper functions, or when locally defining a type that can then be used in the nested function. WG14 is currently considering proposal N3884 that would allow such non-capturing local functions when defined with the static storage class.

But let's consider an example where a nested function accesses a variable of the parent function.


	int foo(int k)
	{
		int bar(int x) { return x + k; }
    		return bar(1);
	}
	

When executed the nested function needs to be able to find the variable k of the parent function (assuming it is not completely optimized away as would be the case here). Traditionally, this was implemented by passing it a pointer to the parent's stack frame, where it then can access the variable at the right stack slot. These techniques were used in PASCAL and similar languages, and x86 even has special instructions, i.e. enter and leave, to support this. However, this is not how GCC implement this feature today.

In GCC, nested functions are lowered in an early middle-end pass. During this pass, all variables of the parent that are accessed by the nested function are collected into a single synthetic structure, and a pointer to this structure is passed to the nested function in a hidden argument. Accesses to such variables are rewritten to access the corresponding member of this structure. The resulting code is essentialy the following (Godbolt Example).


	struct frame { int k; };

	static int bar(struct frame *f, int x)
	{
		return x + f->k;
	}

	int foo(int k)
	{
	    struct frame frame = { k };
	    return bar(&frame, 1);
	}
	

The main advantage of this approach is that this decouples the implementation of nested functions from the rest of the compiler, which can simply treat the static pointer as an additional hidden argument pointing to a regular structure. Other variables of the parent function that are not accessed by any child are not affected at all. Also the frame structure itself can be optimized as any other structure that exists in the program. For example, the example above is simplified to a simple addition by generic optimizer code that does not know anything specific about nested functions.


	"foo":
        	lea     eax, [rdi+1]
        	ret
	

If there are multiple nesting levels, the structure also contains a link to the frame structure one layer up, creating a list (chain) of frame structure, but this is rarely needed.


	

Comparison to C++'s Lambda Feature

It is interesting to compare this to how lambdas work in C++. There are, of course, some superficial differences in how this feature is exposed on the language level. Lambdas are function literals which have no name and are expressions, while GCC's nested functions are regular function definitions that appear in the nested context. But this is not a fundamental difference from an implementation point of view.

Another difference at the language level is that the visible type of the nested function in GCC is a regular function type. In contrast, in C++ the type of a lambda is a Voldemort type, an unique anonymous type that can not be named.

Apart from these two differences, the semantics of nested functions are a subset of C++'s lambda. In fact, the example above can simply be rewritten into C++ by using a lambda object.


	int foo(int k)
	{
		auto bar = [&](int x) -> int { return x + k; };
    		return bar(1);
	}
	

If one looks a bit deeper, the implementation mechanism behind GCC's nested function is also not very different to how a C++ compilers translates a lambda to a callable object: C++'s lambdas are also converted into structures (or rather callable objects in C++) that contain a copy or reference to the captured variables.


	struct bar_anonymous {
		int &k;
		int operator() (int x);
	};

	inline int bar_anonymous::operator() (int x)
	{
		return x + k;
	}

	int foo(int k)
	{
		bar_anonymous bar(k);
		return bar(1);
	}
	

There is still one remaining difference, which can be explained best with an example where there are two nested functions.


	int foo(int k)
	
		int bar1(int x) { return x + 2 * k; }
		int bar2(int x) { return x + 3 * k; }

    		return bar1(1) + bar2(1);
	}
	

In this case, GCC will create a single frame structure in the parent function containing k and both nested functions will receive the exact same pointer to this shared environment.


	struct frame { int k; };

	static int bar1(struct frame *f, int x)
	{
		return x + 2 * f->k;
	}

	static int bar2(struct frame *f, int x)
	{
		return x + 3 * f->k;
	}

	int foo(int k)
	{
	    struct frame frame = { k };
	    return bar1(&frame, 1) + bar2(&frame, 1);
	}
	

In contrast, a C++ compiler will produce two separate objects for each lambda expression, each containing a reference to the same k variable on the stack.


	struct bar1_anonymous {
		int &k;
		int operator() (int x);
	};

	inline int bar1_anonymous::operator() (int x)
	{
		return x + k;
	}

	struct bar2_anonymous {
		int &k;
		int operator() (int x);
	};

	inline int bar2_anonymous::operator() (int x)
	{
		return x + k;
	}

	int foo(int k)
	{
		bar1_anonymous bar1(k);
		bar2_anonymous bar2(k);

		return bar1(1) + bar2(1);
	}
	

Despite this difference in implementation, the GNU C and C++ versions of this example have the exact same semantics.

Conclusion

GCC's nested function correspond to a small semantic subset of C++'s lambda and even though they historically evolved from a different approach, their implementation is not fundamentally different. A compiler that already implements C++ could expose a feature with the same syntax and semantics as GCC's nested functions based on its existing support for lambdas.

Literature

联系我们 contact @ memedata.com