在编译高阶函数调用时,是否有一种标准的处理分离编译和不同类型闭包转换之间交互的方式?
我知道大多数编程语言中存在三种不同编译的类似函数的结构:闭包、(顶层)函数和C++样式的函数对象。它们在语法上被称为相同的方式,但编译器最好生成不同形状的调用站点:
Syntax: | clo(args) | func(args) | obj(args)
--------------------------------------------------------------------------------
Codegen: | clo.fnc(&clo.env, args) | func(args) | cls_call(&obj, args)
^ ^ ^ ^ ^
fn ptr | +--"top level" fn --+ |
+--- "extra" param, compared to source type -----+
在C++中,
cls_call
将是T::operator()
,对于obj
的类T
。C++还允许虚函数对象,但这本质上是带有额外间接性质的闭包情况。此时,对
map (x => x > 3) lst
和map (x => x > y) lst
的调用应该会调用不同的map
函数,因为第一个是简单的函数指针,在提升后,而第二个是一个闭包。我可以想到四种处理这个问题的方法:
1. C++(98)方法,强制被调用者选择调用站点形状(通过形式参数类型:虚函数对象、函数指针或非虚函数对象)或使用模板放弃分离编译,有效地指定下面的解决方案#2。 2. 重载:编译器可以使用适当的名称修饰,多次实例化
map
和所有其他高阶函数。实际上,每个调用站点形状都有一个单独的内部函数类型,并且重载解析选择正确的函数。
3. 强制全局统一的调用站点形状。这意味着所有顶层函数都需要显式的env
参数,即使它们不需要它,还必须引入“额外”的闭包来包装非闭包参数。
4. 保留顶层函数的“自然”签名,但强制所有处理高阶函数参数的操作都通过闭包完成。已经关闭的函数的“额外”闭包调用一个包装器跳板函数来丢弃未使用的env
参数。这似乎比选项3更优雅,但实现起来更困难。编译器要么生成大量的调用约定无关的包装器,要么使用少量的调用约定敏感的thunks...拥有一个优化的闭包转换/lambda lifting混合方案,每个函数可以选择将给定的闭包参数放在env或参数列表中,似乎会使问题更加严重。
问题:
1. 这个问题在文献中有明确的名称吗? 2. 除了上述四种方法之外,还有其他方法吗? 3. 有哪些众所周知的方法之间的权衡?