在闭包中,可变的指针或引用类型如何存储在内存中或在现代函数式语言中处理?

In Closures, how is mutable captured by pointer or reference type stored in memory or handled in modern functional languages?

我正在为教育目的编写一个 Transpiler。 我的转译器从我的语言转译成 C 语言。
我现在正在写闭包语法分析器和代码生成组件。

我看到有人说 C++ 中的闭包实际上被转换为未命名的结构类型,并将捕获的值作为内部变量。

.

这段代码

int c = 10;
auto closure = [=] () -> void {
    std::cout << c << std::endl;
};

基本上在引擎盖下变成了这样的东西,所以他们说。

struct UNNAMED_TYPE_0 {
    int c;

    void operator() () const {
        std::cout << c << std::endl;
    }
};
// assume the closure is initialized and variables are assigned

如果有人想在执行闭包时改变 int c,he/she 必须将此变量作为 ref [&c] () -> void { /* mutation comes here */} 传递。但问题是,如果我们在函数内部声明 int c 并在函数内部创建闭包,就像这样

function<void()> aFunction() {
    int c = 10;
    auto closure = [&c] () -> void { c = 100; }
    return closure;
}

aFunction() ();

int c 被捕获,但一旦 aFunction 堆栈被销毁,int c 也会被销毁。这意味着,如果我们尝试在释放的地址上写入,我们可能会 运行 segmentation fault(core dumped) 指针错误。

Java,

// suppose this callback interface exists
public interface VoidCallback {
    public void method();
} 


public void aMethod() {
    int c = 10;
    VoidCallback callback = () -> c = 10; /* this gives an error */
    // local variables referenced from a lambda expression must be final or effectively final
}

Java 像这样处理闭包并确保闭包捕获没有突变(比方说隐式捕获)。意思是 Java 通过闭包捕获副本而不是引用。对于引用或 class 类型,只有对象指针作为副本传递。虽然指针引用不会改变,但您可以改变指针指向的对象内部的内容。这个和前一个基本一样。

Objective-C,

__block int c = 0;
// they say, this `int c` is allocated in heap instead of stack
// so that it exists until the closure is finished executing.
void (^ closure) (void) = ^void() {
    c = 10; // this is valid
    // this actually changed the `int c`'s value
}; 

Swift

var a : Int = 10;
var closure = { [] () -> Void in
    a = 10; // this is valid by default
    // unless `var a` is declared as `let a`
};

所以,这意味着,Objective-C 和 Swift 将原始捕获列表分配为指针。这样他们就可以变异了。

P.S:请注意 Swift 闭包捕获列表仅适用于 class 或 ref 类型,但我的意思是这里的原始类型的隐式捕获。

这是

__block int c = 0;
// they say, this `int c` is allocated in heap instead of stack
// so that it exists until the closure is finished executing.
void (^ closure) (void) = ^void() {
    c = 10; // this is valid
    // this actually changed the `int c`'s value
}; 

与(基本上)相同

int * c = malloc(sizeof(int));
*c = 0;
void (^ closure) (void) = ^void() {
    *c = 10;
    if (c) {
        free(c);
        c = NULL;
    }
}; 

我认为一旦关闭就释放指针变量太糟糕了。
如果有很多闭包指向变量,执行时会发生变异怎么办?
如果这些闭包被传递或跨不同线程执行怎么办?

我想出了一个使用引用计数技术的解决方案。
当创建一个改变变量的闭包时,变量将被保留。
当改变变量的闭包被销毁时,变量将被释放。
当没有关闭时,变量将真正被释放。 为了确保线程安全,我会在闭包操作引用计数技术时锁定和解锁计数器变量地址。

如果有其他技巧,请指导。
非常感谢任何语言的任何解释。

目前,我对汇编语言的了解为零。

版主们, 因为这个问题是一种研究,所以我请求你不要标记得太宽泛。

以下内容让我印象深刻:"I am writing a Transpiler for educational purpose. My transpiler transpiles from my language to C language."现在,这意味着您的语言规范定义了它应该如何运行!我们无法告诉您您的语言应该如何运作。

现在,您已经找到了一堆选项:

  • C++ 不会对局部变量做任何特殊的事情。如果您保留对它的引用并在它 运行 超出范围时使用它,那就倒霉了。这就是 C++ 的精神,不给你任何开销,一不留神就搬起石头砸自己的脚。
  • Java 只是检查代码并告诉您是否尝试做任何它认为不可靠的事情运行,否则会报错。它不允许你搬起石头砸自己的脚,即使你非常想要它。
  • 其他语言似乎将有限范围的局部变量转换为基于堆的局部变量。我不确定他们的对象模型,但是例如在 Python 你根本没有任何类似于 C++ 局部变量或 Java 原始类型的东西,就像你没有确定性的析构函数调用一样(你使用 with 获得类似的东西,只是为了完整性),所以这没有任何区别。这些语言对您施加了开销,以确保您没有任何悬空引用(也许即使您并不真正需要它)。

现在,第一件事是决定哪一个最适合您的语言的对象模型。只有这样,问题才会出现如何最好地实施它。关于实施,有很多不同的方法。使用引用计数器是一种(尽管使用无锁原子操作实现),使用链表是另一种,或者使用垃圾收集器。