【问题标题】:Is there a a way to achieve closures in C有没有办法在 C 中实现闭包
【发布时间】:2011-05-22 13:52:39
【问题描述】:

我希望它可以工作,但它没有:

#include <stdio.h>

typedef struct closure_s {
  void (*incrementer) ();
  void (*emitter) ();
} closure;

closure emit(int in) {

  void incrementer() {
    in++;
  }

  void emitter() {
    printf("%d\n", in);
  }

  return (closure) {
    incrementer,
    emitter
  };
}

main() {
  closure test[] = {
    emit(10),
    emit(20)
  };

  test[0] . incrementer();
  test[1] . incrementer();

  test[0] . emitter();
  test[1] . emitter();
}

它实际上 确实 编译并为 1 个实例工作......但第二个实例失败了。知道如何在 C 中获得闭包吗?

真的太棒了!

【问题讨论】:

    标签: c closures


    【解决方案1】:

    使用FFCALL

    #include <callback.h>
    #include <stdio.h>
    static void incrementer_(int *in) {
        ++*in;
    }
    static void emitter_(int *in) {
        printf("%d\n", *in);
    }
    int main() {
        int in1 = 10, in2 = 20;
        int (*incrementer1)() = alloc_callback(&incrementer_, &in1);
        int (*emitter1)() = alloc_callback(&emitter_, &in1);
        int (*incrementer2)() = alloc_callback(&incrementer_, &in2);
        int (*emitter2)() = alloc_callback(&emitter_, &in2);
        incrementer1();
        incrementer2();
        emitter1();
        emitter2();
        free_callback(incrementer1);
        free_callback(incrementer2);
        free_callback(emitter1);
        free_callback(emitter2);
    }
    

    但通常在 C 中,您最终会向假闭包传递额外的参数。


    Apple 有一个非标准的 C 扩展名为 blocks,它的工作原理很像闭包。

    【讨论】:

    • 块不只是Objective-C的一部分吗?你能将纯 C 编译为 C(而不仅仅是没有对象或消息的 ObjC)并使用块吗?
    • @outis:我相信 Apple 在所有 C 相关语言中添加了对 GCC 和 clang 的块支持,而不仅仅是 Objective-C。但我没有 OS X 机器来测试它。
    • 你是对的。刚刚测试过,它可以在纯 C 中运行。没有测试 C++,但它可能也可以。
    • 我可能会将此标记为答案。我仍在思考我是否只是一个精致的小把戏,无需借助大量 c 风格的指针和取消引用来获得它的好语法。
    • 尽管修复了明显的拼写错误,但对我来说这并没有接近编译。这是我能找到的唯一使用ffcall 的闭包示例。在放弃让它工作之前,我在手册页上挣扎了很长一段时间。也没有找到libffi 闭包的任何示例,这让我认为没有人将这些库用于闭包。
    【解决方案2】:

    ANSI C 不支持闭包以及嵌套函数。解决方法是使用简单的“结构”。

    两个数字相加的简单示例。

    // Structure for keep pointer for function and first parameter
    typedef struct _closure{
        int x;
        char* (*call)(struct _closure *str, int y);
    } closure;
    
    
    // An function return a result call a closure as string
    char *
    sumY(closure *_closure, int y) {
        char *msg = calloc(20, sizeof(char));
        int sum = _closure->x + y;
        sprintf(msg, "%d + %d = %d", _closure->x, y, sum);
        return msg;
    }
    
    
    // An function return a closure for sum two numbers
    closure *
    sumX(int x) {
        closure *func = (closure*)malloc(sizeof(closure));
        func->x = x;
        func->call = sumY;
        return func;
    }
    

    用法:

    int main (int argv, char **argc)
    {
    
        closure *sumBy10 = sumX(10);
        puts(sumBy10->call(sumBy10, 1));
        puts(sumBy10->call(sumBy10, 3));
        puts(sumBy10->call(sumBy10, 2));
        puts(sumBy10->call(sumBy10, 4));
        puts(sumBy10->call(sumBy10, 5));
    }
    

    结果:

    10 + 1 = 11
    10 + 3 = 13
    10 + 2 = 12
    10 + 4 = 14
    10 + 5 = 15
    

    在 C++11 上,它将通过使用 lambda 表达式来实现。

    #include <iostream>
    int main (int argv, char **argc)
    {
        int x = 10;
        auto sumBy10 = [x] (int y) {
            std::cout << x << " + " << y << " = " << x + y << std::endl;
        };
        sumBy10(1);
        sumBy10(2);
        sumBy10(3);
        sumBy10(4);
        sumBy10(5);
    }
    

    使用标志 -std=c++11 编译后的结果。

    10 + 1 = 11
    10 + 2 = 12
    10 + 3 = 13
    10 + 4 = 14
    10 + 5 = 15
    

    【讨论】:

    • 你分配了内存但从未释放它们
    • @phuclv,没关系,因为操作系统会在sumBy10(5); 行完成后释放它。永远不要做你的操作系统会免费为你做的额外工作。
    • @jdk1.0 在这种情况下对于main 中的代码可能没问题,但这是一个坏主意,因为您会在没有通知的情况下习惯它并忘记释放其他分配的内存。它还可以防止您使用检漏仪。如果代码被移动到一个函数并在需要大量内存之前调用并且您不知道您必须添加 free 怎么办?由于这个原因,我已经看到了生产错误。 Should I free memory before exit?
    【解决方案3】:

    带有 JavaScript 示例的闭包的工作定义

    闭包是一种对象,它包含指向要执行的函数的指针或某种类型的引用以及函数所需数据的实例。

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures 的 JavaScript 示例是

    function makeAdder(x) {
      return function(y) { // create the adder function and return it along with
        return x + y;      // the captured data needed to generate its return value
      };
    }
    

    然后可以像这样使用:

    var add5 = makeAdder(5);  // create an adder function which adds 5 to its argument
    
    console.log(add5(2));  // displays a value of 2 + 5 or 7
    

    用 C 克服的一些障碍

    C 编程语言是一种静态类型的语言,与 JavaScript 不同,它也没有垃圾收集和其他一些特性,可以轻松地在 JavaScript 或其他对闭包的内在支持的语言中进行闭包。

    标准 C 中闭包的一大障碍是缺乏语言支持 JavaScript 示例中的构造类型,其中闭包不仅包括函数,还包括创建闭包时捕获的数据副本,一种保存状态的方法,然后可以在执行闭包时使用,以及在调用闭包函数时提供的任何其他参数。

    但是 C 确实有一些基本的构建块可以提供创建一种闭包的工具。一些困难是(1)内存管理是程序员的职责,没有垃圾收集,(2)函数和数据是分开的,没有类或类类型机制,(3)静态类型,所以没有数据类型的运行时发现或数据大小,以及 (4) 在创建闭包时捕获状态数据的语言设施较差。

    使 C 的闭包功能成为可能的一件事是 void * 指针并使用 unsigned char 作为一种通用内存类型,然后通过强制转换将其转换为其他类型。

    使用标准 C 的实现,并在此处和那里进行一些扩展

    注意: 以下示例依赖于大多数 x86 32 位编译器使用的基于堆栈的参数传递约定。大多数编译器还允许指定调用约定,而不是基于堆栈的参数传递,例如 Visual Studio 的 __fastcall 修饰符。 x64 和 64 位 Visual Studio 的默认设置是默认使用 __fastcall 约定,以便函数参数在寄存器中传递,而不是在堆栈中传递。请参阅 Microsoft MSDN 中的 Overview of x64 Calling Conventions 以及 How to set function arguments in assembly during runtime in a 64bit application on Windows? 以及 How are variable arguments implemented in gcc? 中的各种答案和 cmets。

    我们可以做的一件事是解决这个问题,为 C 提供某种闭包工具是为了简化问题。提供对大多数应用程序有用的 80% 解决方案总比没有解决方案要好。

    这样一种简化是只支持不返回值的函数,换句话说,函数声明为void func_name()。我们还将放弃函数参数列表的编译时类型检查,因为这种方法在运行时构建函数参数列表。我们放弃的这些东西都不是微不足道的,所以问题是这种 C 语言闭包方法的价值是否超过了我们放弃的东西。

    首先让我们定义我们的闭包数据区。闭包数据区表示我们将用于包含闭包所需信息的内存区域。我能想到的最少数据量是指向要执行的函数的指针,以及作为参数提供给函数的数据副本。

    在这种情况下,我们将提供函数所需的任何捕获状态数据作为函数的参数。

    我们还希望有一些基本的安全防护措施,以便我们能够合理地安全失败。不幸的是,在我们用来实现某种闭包形式的一些变通方法中,安全栏有点弱。

    源代码

    以下源代码是使用 Visual Studio 2017 社区版在 .c C 源文件中开发的。

    数据区是一个结构体,包含一些管理数据、一个指向函数的指针和一个开放式数据区。

    typedef struct {
        size_t  nBytes;    // current number of bytes of data
        size_t  nSize;     // maximum size of the data area
        void(*pf)();       // pointer to the function to invoke
        unsigned char args[1];   // beginning of the data area for function arguments
    } ClosureStruct;
    

    接下来我们创建一个初始化闭包数据区的函数。

    ClosureStruct * beginClosure(void(*pf)(), int nSize, void *pArea)
    {
        ClosureStruct *p = pArea;
    
        if (p) {
            p->nBytes = 0;      // number of bytes of the data area in use
            p->nSize = nSize - sizeof(ClosureStruct);   // max size of the data area
            p->pf = pf;         // pointer to the function to invoke
        }
    
        return p;
    }
    

    此函数旨在接受指向数据区域的指针,这为函数的用户希望如何管理内存提供了灵活性。它们可以使用堆栈上的一些内存或静态内存,也可以通过malloc() 函数使用堆内存。

    unsigned char closure_area[512];
    ClosureStruct *p = beginClosure (xFunc, 512, closure_area);
    

    ClosureStruct *p = beginClosure (xFunc, 512, malloc(512));
    //  do things with the closure
    free (p);  // free the malloced memory.
    

    接下来我们提供一个函数,允许我们向闭包添加数据和参数。该函数的目的是建立闭包数据,以便在调用闭包函数时,将为闭包函数提供其工作所需的任何数据。

    ClosureStruct * pushDataClosure(ClosureStruct *p, size_t size, ...)
    {
        if (p && p->nBytes + size < p->nSize) {
            va_list jj;
    
            va_start(jj, size);    // get the address of the first argument
    
            memcpy(p->args + p->nBytes, jj, size);  // copy the specified size to the closure memory area.
            p->nBytes += size;     // keep up with how many total bytes we have copied
            va_end(jj);
        }
    
        return p;
    }
    

    为了使它更易于使用,让我们提供一个包装宏,它通常很方便,但由于它是 C 处理器文本操作,所以有一些限制。

    #define PUSHDATA(cs,d) pushDataClosure((cs),sizeof(d),(d))
    

    所以我们可以使用类似下面的源代码:

    unsigned char closurearea[256];
    int  iValue = 34;
    
    ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, closurearea), iValue);
    dd = PUSHDATA(dd, 68);
    execClosure(dd);
    

    调用闭包:execClosure() 函数

    最后一部分是execClosure() 函数,它使用其数据执行闭包函数。我们在这个函数中所做的是在调用函数时将闭包数据结构中提供的参数列表复制到堆栈中。

    我们所做的是将闭包数据的 args 区域转换为指向包含 unsigned char 数组的结构的指针,然后取消引用该指针,以便 C 编译器在调用之前将参数的副本放入堆栈中闭包中的函数。

    为了更轻松地创建 execClosure() 函数,我们将创建一个宏,以便轻松创建所需的各种大小的结构。

    // helper macro to reduce type and reduce chance of typing errors.
    
    #define CLOSEURESIZE(p,n)  if ((p)->nBytes < (n)) { \
    struct {\
    unsigned char x[n];\
    } *px = (void *)p->args;\
    p->pf(*px);\
    }
    

    然后我们使用这个宏来创建一系列的测试来确定如何调用闭包函数。此处选择的尺寸可能需要针对特定​​应用进行调整。这些大小是任意的,并且由于闭包数据很少具有相同的大小,因此不能有效地使用堆栈空间。并且有可能存在比我们允许的更多的关闭数据。

    // execute a closure by calling the function through the function pointer
    // provided along with the created list of arguments.
    ClosureStruct * execClosure(ClosureStruct *p)
    {
        if (p) {
            // the following structs are used to allocate a specified size of
            // memory on the stack which is then filled with a copy of the
            // function argument list provided in the closure data.
            CLOSEURESIZE(p,64)
            else CLOSEURESIZE(p, 128)
            else CLOSEURESIZE(p, 256)
            else CLOSEURESIZE(p, 512)
            else CLOSEURESIZE(p, 1024)
            else CLOSEURESIZE(p, 1536)
            else CLOSEURESIZE(p, 2048)
        }
    
        return p;
    }
    

    我们返回指向闭包的指针以使其易于使用。

    使用开发的库的示例

    我们可以如下使用上面的。首先是几个并没有多大作用的示例函数。

    int zFunc(int i, int j, int k)
    {
        printf("zFunc i = %d, j = %d, k = %d\n", i, j, k);
        return i + j + k;
    }
    
    typedef struct { char xx[24]; } thing1;
    
    int z2func(thing1 a, int i)
    {
        printf("i = %d, %s\n", i, a.xx);
        return 0;
    }
    

    接下来我们构建闭包并执行它们。

    {
        unsigned char closurearea[256];
        thing1 xpxp = { "1234567890123" };
        thing1 *ypyp = &xpxp;
        int  iValue = 45;
    
        ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
        free(execClosure(PUSHDATA(dd, iValue)));
    
        dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
        dd = PUSHDATA(dd, 68);
        execClosure(dd);
    
        dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
        dd = PUSHDATA(dd, 145);
        dd = PUSHDATA(dd, 185);
        execClosure(dd);
    }
    

    输出为

    i = 45, 1234567890123
    i = 68, 1234567890123
    zFunc i = 45, j = 145, k = 185
    

    那么柯里化呢?

    接下来我们可以对闭包结构进行修改,以允许我们对函数进行柯里化。

    typedef struct {
        size_t  nBytes;    // current number of bytes of data
        size_t  nSize;     // maximum size of the data area
        size_t  nCurry;    // last saved nBytes for curry and additional arguments
        void(*pf)();       // pointer to the function to invoke
        unsigned char args[1];   // beginning of the data area for function arguments
    } ClosureStruct;
    

    支持currying和curry point的重置功能

    ClosureStruct *curryClosure(ClosureStruct *p)
    {
        p->nCurry = p->nBytes;
        return p;
    }
    ClosureStruct *resetCurryClosure(ClosureStruct *p)
    {
        p->nBytes = p->nCurry;
        return p;
    }
    

    测试的源代码可以是:

    {
        unsigned char closurearea[256];
        thing1 xpxp = { "1234567890123" };
        thing1 *ypyp = &xpxp;
        int  iValue = 45;
    
        ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
        free(execClosure(PUSHDATA(dd, iValue)));
        dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
        dd = PUSHDATA(dd, 68);
        execClosure(dd);
        dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
        dd = PUSHDATA(dd, 145);
        dd = curryClosure(dd);
        dd = resetCurryClosure(execClosure(PUSHDATA(dd, 185)));
        dd = resetCurryClosure(execClosure(PUSHDATA(dd, 295)));
    }
    

    的输出
    i = 45, 1234567890123
    i = 68, 1234567890123
    zFunc i = 45, j = 145, k = 185
    zFunc i = 45, j = 145, k = 295
    

    【讨论】:

      【解决方案4】:

      GCC 和 clang 有块扩展,本质上是 C 中的闭包。

      【讨论】:

      • 区块不在 GCC 主线,只有苹果的 fork。
      • Apple 已经在 Apple 自己的 GCC 源代码副本和 Clang LLVM 编译器前端中实现了块。
      【解决方案5】:

      GCC 支持内部函数,但不支持闭包。 C++0x 将有闭包。据我所知,没有任何 C 版本,当然也没有标准版本,可以提供这种级别的出色。

      Phoenix 是 Boost 的一部分,在 C++ 中提供闭包。

      【讨论】:

      • 你能想出一个巧妙的方法来按摩 C 以实现我们在这里寻找的东西,而不会污染语法或需要额外的库吗?听起来很接近。
      • 没有。由于没有语言级别的支持,因此您需要库支持。由于最接近于扩展语言本身的是使用宏并且没有重载,因此您需要特殊的语法,可能隐藏在宏后面。
      • 好吧,我知道这不是 C 的预期特性 --- 但是通过一堆结构和宏魔法进行面向对象的继承也不是计划中的用途。问题在于地址空间的重用和指针的临时性……关键原因很简单,“C 不是那种语言”。即使你把私有函数和变量部分弄下来;这就是 OO 所提供的,你仍然缺少匿名函数——这是使它有用的主要组成部分;这无疑需要宏。
      • C 可以支持其他风格的编程,但您仍然必须依赖 C 的功能和过程语法。甚至 C 中的 OO 风格的例子仍然依赖于指针和过程。闭包仍然超出了 C 的能力;如果您查看 FFCALL 的源代码,它使用汇编语言来构建闭包。 FFCALL 尽可能干净。
      【解决方案6】:

      在此页面上,您可以找到有关如何在 C 中执行闭包的说明:

      http://brodowsky.it-sky.net/2014/06/20/closures-in-c-and-scala/

      这个想法是需要一个结构并且该结构包含函数指针,但作为第一个参数提供给函数。除了它需要大量的样板代码和内存管理当然是个问题之外,它可以工作并提供其他语言闭包的功能和可能性。

      【讨论】:

        【解决方案7】:

        回答

        #include <stdio.h>
        #include <stdlib.h>
        
        /*
        File Conventions
        ----------------
        alignment: similar statements only
                   int    a = 10;
                   int* omg = {120, 5};
        functions: dofunction(a, b, c);
        macros:    _do_macro(a, b, c);
        variables: int dovariable=10;
        */
        
        
        ////Macros
        
        #define _assert(got, expected, teardownmacro) \
          do { \
            if((got)!=(expected)) { \
              fprintf(stderr, "line %i: ", __LINE__); \
              fprintf(stderr, "%i != %i\n", (got), (expected)); \
              teardownmacro; \
              return EXIT_FAILURE; \
            } \
          } while(0);
        
        
        ////Internal Helpers
        
        static void istarted() {
          fprintf(stderr, "Start tests\n");
        }
        
        static void iended() {
          fprintf(stderr, "End tests\n");
        }
        
        ////Tests
        
        int main(void)
        {
          ///Environment
        
          int  localvar = 0;
          int* localptr = NULL;
        
        
          ///Closures
        
        #define _setup_test(mvar, msize) \
          do { \
            localptr=calloc((msize), sizeof(int)); \
            localvar=(mvar); \
          } while(0);
        
        #define _teardown_test() \
          do { \
            free(localptr); \
            localptr=NULL; \
          } while(0);
        
          
          ///Tests
          
          istarted();
        
          _setup_test(10, 2);
          _assert(localvar, 10, _teardown_test());
          _teardown_test();
        
          _setup_test(100, 5);
          _assert(localvar, 100, _teardown_test());
          _teardown_test();
        
          iended();
        
          return EXIT_SUCCESS;
        }
        

        上下文

        我很好奇其他人是如何在 C 中实现这一点的。当我没有看到这个答案时,我并不完全感到惊讶。 警告:此答案不适合初学者。

        我更多地生活在 Unix 的思维方式中:我的很多个人程序和库都很小,并且可以很好地完成一件事。 作为“闭包”的宏在这种情况下要安全得多。我相信所有可读性的组织和指定约定都非常重要,因此代码以后我们可以阅读,宏看起来像宏,函数看起来像函数。澄清一下,不是字面上的这些个人约定,只是有一些被指定和遵循以区分不同的语言结构(宏和函数)。无论如何,我们都应该这样做。

        不要害怕宏。 当它有意义时:使用它们。高级部分是时间。我的例子是时间的一个例子。它们非常强大,而且没有那么可怕。


        漫无目的

        我有时会在其他语言中使用适当的闭包/lambda 在函数中一遍又一遍地执行一组表达式。这是一个小的上下文感知私人助手功能。不管它的正确定义如何,这都是闭包可以做的事情。它帮助我编写更少的代码。这样做的另一个好处是您无需引用结构即可知道如何使用它或了解它在做什么。其他答案没有这个好处,而且,如果不是很明显,我认为可读性非常高。我力求简单易读的解决方案。这一次我写了一个 iOS 应用程序,它非常棒,而且我能做到的尽可能简单。然后我用 5 行代码在 bash 中编写了相同的“应用程序”并被诅咒。

        还有嵌入式系统。

        【讨论】:

        • 我完全感觉到你的“也是嵌入式系统”评论在我的内心深处。在我的另一个浏览器选项卡中,我提供了来自开发人员的代码审查反馈,该开发人员完全没有获得用于嵌入式系统的分层库设计。我对一个头文件的总 cmets 比头文件中的行数要多。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-19
        • 1970-01-01
        • 1970-01-01
        • 2021-04-21
        • 1970-01-01
        相关资源
        最近更新 更多