【问题标题】:Implementing RAII in pure C?在纯 C 中实现 RAII?
【发布时间】:2010-09-26 23:19:31
【问题描述】:

是否可以在纯 C 中实现RAII

我认为这是不可能的,但也许可以使用某种肮脏的技巧。想到重载标准的free 函数,或者可能会覆盖堆栈上的返回地址,以便当函数返回时,它会调用其他一些以某种方式释放资源的函数?或者也许有一些 setjmp/longjmp 技巧?

这纯粹是学术兴趣,我无意实际编写如此不可移植和疯狂的代码,但我想知道这是否可能。

【问题讨论】:

  • 不能简单的覆盖栈上的返回地址;您必须保留输入时的值,然后用替代方法覆盖它。丑陋,但可能有效。考虑对内存使用基于竞技场的内存分配。否则,请非常小心(并担心中断!)。
  • 在没有异常的情况下,RAII 有用吗? (只是问)
  • @JoshPetitt 当然,早点回归,只是不必记住释放每一件事 = 更少的错误。
  • @JoshPetitt 你至少必须少写一个语句。例如 fopen 没有对应的 fclose
  • 我很惊讶没有人建议您使用 C++ 编译器,并用 C++ 可编译的那种神秘的 C 方言编写(仅在需要时使用 RAII 功能)。我也很惊讶你没有接受约翰内斯的回答,除非你坚持一个“更普遍”的解决方案。

标签: c raii


【解决方案1】:
my implementation of raii for c in pure c and minimal asm
@ https://github.com/smartmaster/sml_clang_raii

**RAII for C language in pure C and ASM**

**featurs : **

-easy and graceful to use
- no need seperate free cleanup functions
- able to cleanup any resources or call any function on scope exits


**User guide : **

-add source files in src folder to your project
-include sml_raii_clang.h in.c file
-annote resource and its cleanup functions

/* 示例代码 */

void sml_raii_clang_test()
{
    //start a scope, the scope name can be any string
    SML_RAII_BLOCK_START(0);


    SML_RAII_VOLATILE(WCHAR*) resA000 = calloc(128, sizeof(WCHAR)); //allocate memory resource
    SML_RAII_START(0, resA000); //indicate starting a cleanup code fragment, here 'resA000' can be any string you want
    if (resA000) //cleanup code fragment
    {
        free(resA000);
        resA000 = NULL;
    }
    SML_RAII_END(0, resA000); //indicate end of a cleanup code fragment


    //another resource
    //////////////////////////////////////////////////////////////////////////
    SML_RAII_VOLATILE(WCHAR*) res8000 = calloc(128, sizeof(WCHAR));
    SML_RAII_START(0, D000);
    if (res8000)
    {
        free(res8000);
        res8000 = NULL;
    }
    SML_RAII_END(0, D000);


    //scope ended, will call all annoated cleanups
    SML_RAII_BLOCK_END(0);
    SML_RAII_LABEL(0, resA000); //if code is optimized, we have to put labels after SML_RAII_BLOCK_END
    SML_RAII_LABEL(0, D000);
}

【讨论】:

    【解决方案2】:

    将 RAII 引入 C 的一种解决方案(当您没有 cleanup() 时)是使用将执行清理的代码包装您的函数调用。这也可以打包成一个整洁的宏(在最后展示)。

    /* Publicly known method */
    void SomeFunction() {
      /* Create raii object, which holds records of object pointers and a
         destruction method for that object (or null if not needed). */
      Raii raii;
      RaiiCreate(&raii);
    
      /* Call function implementation */
      SomeFunctionImpl(&raii);
    
      /* This method calls the destruction code for each object. */
      RaiiDestroyAll(&raii);
    }
    
    /* Hidden method that carries out implementation. */
    void SomeFunctionImpl(Raii *raii) {
      MyStruct *object;
      MyStruct *eventually_destroyed_object;
      int *pretend_value;
    
      /* Create a MyStruct object, passing the destruction method for
         MyStruct objects. */
      object = RaiiAdd(raii, MyStructCreate(), MyStructDestroy);
    
      /* Create a MyStruct object (adding it to raii), which will later
         be removed before returning. */
      eventually_destroyed_object = RaiiAdd(raii,
          MyStructCreate(), MyStructDestroy);
    
      /* Create an int, passing a null destruction method. */
      pretend_value = RaiiAdd(raii, malloc(sizeof(int)), 0);
    
      /* ... implementation ... */
    
      /* Destroy object (calling destruction method). */
      RaiiDestroy(raii, eventually_destroyed_object);
    
      /* or ... */
      RaiiForgetAbout(raii, eventually_destroyed_object);
    }
    

    您可以使用宏来表达SomeFunction 中的所有样板代码,因为每次调用都相同。

    例如:

    /* Declares Matrix * MatrixMultiply(Matrix * first, Matrix * second, Network * network) */
    RTN_RAII(Matrix *, MatrixMultiply, Matrix *, first, Matrix *, second, Network *, network, {
      Processor *processor = RaiiAdd(raii, ProcessorCreate(), ProcessorDestroy);
      Matrix *result = MatrixCreate();
      processor->multiply(result, first, second);
      return processor;
    });
    
    void SomeOtherCode(...) {
      /* ... */
      Matrix * result = MatrixMultiply(first, second, network);
      /* ... */
    }
    

    注意:您可能希望使用 P99 等高级宏框架来实现上述目标。

    【讨论】:

    • 必须显式调用方法 (RaiiDestroyAll) 有点违背 raii 的理念。
    • 这是通过语言的机制。如果您愿意,您可以使用宏隐藏显式调用,例如 RTN_RAII(int, func_name, int, arg0, int, arg1, {/* code */})(您可以使用 P99 进行宏的繁重工作)。
    • en.cppreference.com/w/cpp/language/raii "该技术的另一个名称是作用域绑定资源管理 (SBRM),在 RAII 对象的生命周期因作用域退出而结束的基本用例之后。" Bjarne Stroustrup 说“RAII 对这个概念来说是个坏名字……更好的名字可能是:构造函数获取,析构函数发布” 关键是发布是自动的,无论如何。关键是您不应该进行清理调用。这就是RAII的定义。我听说有些 C 编译器提供类似扩展的功能,但 C 本身做不到。
    • 显然 C 不能进行基于作用域的自动清理。从这个问题:“我认为这不可能以任何理智的方式,但也许有可能使用某种肮脏的把戏”。我提供的是一种作为解决方法的机制,您可以借助一个简单的宏在 C 中获得 managed 清理,您不必手动调用 destroy 或清理(清理将完成在宏中),但您需要注册指针。
    • 即使是 C++ 也需要您以某种方式标记作用域的结尾,通常使用右大括号。在我看来,RaiiDestroyAll() 只是为了同样的目的,除了它与右括号分离。可以将它们与宏重新耦合,但我认为这不会让事情变得更好:分别隐藏和不隐藏实现细节是 C++ 和 C 的本质,所以在我看来这很合适在 C 中处理 RAII 的方法(而不是一个肮脏的技巧)。单独的大括号和 RAII 范围可以让你做一些事情,例如在几个大括号上使用一个 RAII 范围。
    【解决方案3】:

    为了补充约翰内斯回答的这一部分:

    cleanup 属性在变量超出范围时运行一个函数

    cleanup 属性有一个限制(http://gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Variable-Attributes.html):该属性只能应用于自动函数作用域变量。

    因此,如果文件中有静态变量,则可以通过这种方式为静态变量实现 RAII:

    #include <stdio.h>
    #include <stdlib.h>
    
    static char* watched2;
    
    __attribute__((constructor))
    static void init_static_vars()
    {
      printf("variable (%p) is initialazed, initial value (%p)\n", &watched2, watched2);
      watched2=malloc(1024);
    }
    
    
    __attribute__((destructor))
    static void destroy_static_vars()
    {
      printf("variable (%p), value( %p) goes out of scope\n", &watched2, watched2);
      free(watched2);
    }
    
    int main(void)
    {
      printf("exit from main, variable (%p) value(%p) is static\n", &watched2, watched2);
      return 0;
    }
    

    这是一个测试:

    >./example
    variable (0x600aa0) is initialazed, initial value ((nil))
    exit from main, variable (0x600aa0) value(0x16df010) is static
    variable (0x600aa0), value( 0x16df010) goes out of scope
    

    【讨论】:

      【解决方案4】:

      我以前不知道属性清理。当然是一个适用的巧妙解决方案,但它似乎不适用于基于 setjmp/longjmp 的异常实现;不会为引发异常的范围和捕获它的范围之间的任何中间范围/函数调用清理函数。 Alloca 没有这个问题,但是使用 alloca,您不能将内存块的所有权从调用它的函数转移到外部范围,因为内存是从堆栈帧分配的。可以实现类似于 C++ unique_ptr 和 shared_ptr 的智能指针,认为它需要使用宏括号而不是 {} 并返回才能将额外的逻辑关联到范围进入/退出。有关实现,请参阅 https://github.com/psevon/exceptions-and-raii-in-c 中的 autocleanup.c。

      【讨论】:

        【解决方案5】:

        检查 https://github.com/psevon/exceptions-and-raii-in-c 以获取唯一和共享智能指针和异常的 C 实现。此实现依赖于宏括号 BEGIN ... END 替换大括号并检测超出范围的智能指针,以及用于返回的宏替换。

        【讨论】:

          【解决方案6】:

          如果您的编译器支持 C99(甚至大部分),您可以使用可变长度数组 (VLA),例如:

          int f(int x) { 
              int vla[x];
          
              // ...
          }
          

          如果没有记错的话,gcc 在添加到 C99 之前就已经/支持此功能。这(大致)等同于以下简单情况:

          int f(int x) { 
              int *vla=malloc(sizeof(int) *x);
              /* ... */
              free vla;
          }
          

          但是,它不允许您执行 dtor 可以执行的任何其他操作,例如关闭文件、数据库连接等。

          【讨论】:

          • 请注意 1) 堆栈通常比堆更受限制; 2)您基本上无法从堆栈溢出中恢复(您将获得一个您无法处理的 SIGSEGV)。失败的 malloc 将返回 nullptr,而失败的 new 将抛出 std::bad_alloc。
          • @RaúlSalinas-Monteagudo 你所说的“更有限”是什么意思?
          • @jb:根据操作系统的不同,堆栈的大小通常被限制在几兆字节左右。但他只说对了一半。例如,在 Linux 上,一个失败的 malloc(或新的)会经常导致 OOMKILLER 运行,这可能会简单地杀死有问题的程序(或者可能会杀死其他程序以释放足够的内存以使分配成功)。因此,尽管堆通常更大,但尝试使用超过可用的内存也可能无法恢复。
          • @jb:如果我没记错的话,堆栈大小(默认情况下)正常进程为 8 MB,线程为 2 MB。如果你开始在堆栈中创建数组,你很可能最终会溢出它。但这当然取决于日期的性质以及嵌套调用的深度。我不会为了一个可靠的程序而冒险。
          • @RaúlSalinas-Monteagudo 好点。对于大多数应用程序来说,8 MB(甚至 2 MB)实际上是相当多的内存,我必须承认我从来没有遇到过任何麻烦。但是,我刚刚编写了一个简单的测试程序,并且能够通过在堆栈上分配一个 10 MB 的数组来立即使它崩溃。将来一定会牢记这一点。谢谢!
          【解决方案7】:

          你看过 alloca() 吗?当 var 离开作用域时,它将释放。但是要有效地使用它,调用者必须始终在将其发送到事物之前执行 alloca...如果您正在实现 strdup,那么您不能使用 alloca。

          【讨论】:

          • alloca() 并不是真正的纯 C。它不在任何 C 标准中,因此在 C 可用的任何地方都不可用。例如,它在 Microsoft 的 Windows C 编译器中不可用。请参阅C FAQ
          • 当变量离开其作用域时它不会释放。它在函数退出时释放,这使得它非常危险。
          【解决方案8】:

          这是固有的实现依赖,因为标准不包括这种可能性。对于 GCC,cleanup 属性在变量超出范围时运行一个函数:

          #include <stdio.h>
          
          void scoped(int * pvariable) {
              printf("variable (%d) goes out of scope\n", *pvariable);
          }
          
          int main(void) {
              printf("before scope\n");
              {
                  int watched __attribute__((cleanup (scoped)));
                  watched = 42;
              }
              printf("after scope\n");
          }
          

          打印:

          before scope
          variable (42) goes out of scope
          after scope
          

          here

          【讨论】:

          • 这比我想象的要整洁!
          • 有关信息,至少从 GCC 4.0.0 开始支持此功能
          • 这似乎也适用于 clang 11.0.0,尽管它没有在属性参考中列出。
          • ⁺¹。注意:您可以在声明变量的同一行初始化变量。
          【解决方案9】:

          可能最简单的方法是使用 goto 跳转到函数末尾的标签,但对于您正在查看的内容而言,这可能过于手动。

          【讨论】:

            【解决方案10】:

            我会选择覆盖堆栈上的返回地址。它会成为最透明的。替换 free 仅适用于堆分配的“对象”。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2011-07-10
              • 2012-12-11
              • 2012-03-10
              • 1970-01-01
              • 2011-12-20
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多