在纯C中实现RAII?

84

在纯C中实现RAII可能吗?

我认为以任何明智的方式都不可能,但也许可以使用某种肮脏的技巧。比如重载标准的free函数,或者覆盖堆栈上的返回地址,使得函数返回时调用其他某个释放资源的函数?或者通过一些setjmp/longjmp技巧?

这只是纯粹的学术兴趣,我没有真正编写这种不可移植和疯狂的代码的意图,但我想知道是否有可能。


你不能简单地在堆栈上覆盖返回地址;你必须保留进入时的值,然后用替代值覆盖它。这很丑陋,但可能有效。 考虑使用基于区域的内存分配来管理内存。否则,只需非常小心(并担心中断!)。 - Jonathan Leffler
在没有异常的情况下,RAII还有用吗?(只是问问) - Josh Petitt
3
当然,早期返回和不需要记住释放每一个东西 = 更少的错误。 - Oktalist
@JoshPetitt 至少你可以少写一条语句。例如,没有相应的 fclose 的 fopen。 - Justin Meiners
我很惊讶没有人建议您使用C++编译器,并以可由C++编译的古怪C方言编写代码(只在需要时使用RAII特性)。我也很惊讶您还没有接受Johannes的答案,除非您正在等待“更一般化”的解决方案。 - jxh
为什么要坚持使用C语言并做一些奇怪的事情呢?如果可以的话,只需开始将代码移植到C++中即可。说服你的老板... - Raúl Salinas-Monteagudo
10个回答

112

这是固有的实现依赖,因为标准没有包括这种可能性。对于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

请参见此处


17
这比我想象中能做的更整洁! - elifiner
请注意,此功能至少从GCC 4.0.0版本开始支持。 - Penghe Geng
1
这在clang 11.0.0中似乎也可以工作,尽管它没有列在属性参考中。 - j b
注意:您可以在声明变量的同一行上初始化变量。 - Hi-Angel

15

在没有cleanup()的情况下,将RAII引入C的一种解决方案是使用包装函数调用的代码进行清理。这也可以封装在一个整洁的宏中(在末尾显示)。

/* 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)来使上述操作成为可能。

3
需要显式调用一个方法(RaiiDestroyAll)有点违背了 RAII 的核心思想。 - Mooing Duck
这是通过语言实现的机制。如果你想的话,可以使用宏来隐藏显式调用,例如 RTN_RAII(int, func_name, int, arg0, int, arg1, {/* code */})(你可以使用 P99 来完成宏的重活)。 - Keldon Alleyne
2
这种技术的另一个名称是作用域绑定资源管理(SBRM),因为基本用例是由于作用域退出而结束RAII对象的生命周期。 Bjarne Stroustrup说:“RAII是该概念的不好名称...更好的名称可能是:构造函数获取,析构函数释放。” 关键是释放是自动的,无论如何。关键是你不应该进行清理调用。这就是RAII的定义。 我听说一些C编译器提供类似的扩展,但C本身不能做到。 - Mooing Duck
3
显然,C语言无法进行基于作用域的自动清理。从问题中可以看出:"我假设用任何正常的方法都不可能实现,但也许可以使用某种肮脏的技巧来实现"。我提供的是一种机制,通过一个简单的宏,您可以在C语言中获得管理的清理,而无需手动调用destroy或清理(清理将在宏中完成),但您需要注册指针。 - Keldon Alleyne
1
即使是C++,你也需要用一个闭合括号来标记作用域的结束。在我看来,RaiiDestroyAll()只是起到了同样的作用,只不过它与闭合括号解耦了。你可以用宏重新将它们耦合起来,但我认为这并不能让事情变得更好:C++和C分别隐藏和不隐藏实现细节,所以这似乎是一种适当的方法(而不是肮脏的技巧)来在C中使用RAII。而独立的括号和RAII作用域让你可以做一些事情,比如在几个括号作用域中使用一个RAII作用域。 - michaeljt
回复我的上一条评论。当RAII类似的内存管理与本地堆栈框架作用分离时,可以做什么的一个现实世界的例子是Linux内核devm_kcalloc()和相关函数[1]。是否有必要在上面的答案中提到这一点呢?[1] https://www.mjmwired.net/kernel/Documentation/driver-model/devres.txt - michaeljt

9
如果您的编译器支持C99(甚至是其大部分特性),则可以使用可变长度数组(VLA),例如:
int f(int x) { 
    int vla[x];

    // ...
}

如果我的记忆没有出错,gcc在C99标准之前就已经有/支持了这个功能。这大致相当于以下简单情况:

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

然而,它不允许你做dtor可以做的其他事情,比如关闭文件、数据库连接等。


3
请注意:1)堆栈的大小通常比堆要小得多;2)堆栈溢出基本上是无法恢复的(你会收到一个SIGSEGV信号,你不能处理)。如果malloc失败,则返回nullptr,而如果new失败,则会抛出std::bad_alloc异常。 - Raúl Salinas-Monteagudo
@RaúlSalinas-Monteagudo 你所说的“更加有限”是什么意思? - j b
1
@jb:根据操作系统,堆栈的大小通常仅限于几兆字节左右。但他只说对了一半。例如,在Linux上,失败的malloc(或new)经常会导致OOMKILLER运行,这可能会直接结束相关程序(也可能杀死其他程序以释放足够的内存以使分配成功)。因此,尽管堆通常更大,但尝试使用超出其可用范围的空间也可能是无法恢复的。 - Jerry Coffin
1
@jb:如果我没记错的话,堆栈大小(默认情况下)为普通进程为8 MB,线程为2 MB。如果您开始在堆栈中创建数组,那么很可能会溢出。但这当然取决于您的数据性质以及您嵌套调用的深度。对于可靠的程序,我不会冒这样的危险。 - Raúl Salinas-Monteagudo
1
@RaúlSalinas-Monteagudo 说得好。对于大多数应用程序来说,8 MB(甚至2 MB)的内存实际上已经足够了,我必须承认我从未遇到过任何问题。然而,我刚刚编写了一个简单的测试程序,并且通过在堆栈上分配一个10 MB的数组,立即使其崩溃。将来一定会记住这点。谢谢! - j b
只要你不涉及文件操作,那么这个大小就还好,但是一旦涉及到文件操作,8MB 的空间大部分情况下都是太小了。 - Luxalpa

4

可能最简单的方法是使用goto跳转到函数末尾的标签,但这对于你想要的那种事情来说可能太繁琐了。


1

我会选择覆盖栈上的返回地址。这是最透明的方法。替换free只适用于堆分配的“对象”。


1
你看过alloca()吗?它会在变量离开作用域时释放。但要有效地使用它,调用者必须始终在将其发送到其他地方之前进行alloca。如果你正在实现strdup,那么你不能使用alloca。

3
alloca()并非纯粹的C语言,它没有被包含在任何C标准中,因此在C可用的所有地方都不可用。例如,在Windows上,Microsoft的C编译器就不支持它。请参阅C FAQ - hippietrail
2
当一个变量离开其作用域时,它并不会被释放。它会在函数退出时被释放,这使得它非常危险。 - Bauss

1
为了补充Johannes回答的部分: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

0

请查看https://github.com/psevon/exceptions-and-raii-in-c,了解C语言实现的独占和共享智能指针以及异常处理。该实现依赖于宏括号BEGIN ... END替换大括号,并检测智能指针是否超出作用域,同时使用宏替换return语句。


0

我之前不知道属性清理。当适用时,这确实是一个很好的解决方案,但它似乎与基于setjmp/longjmp的异常实现不兼容;清理函数不会为抛出异常的作用域和捕获它的作用域之间的任何中间作用域/函数调用。Alloca没有这个问题,但使用alloca分配的内存块无法从调用它的函数转移到外部作用域,因为内存是从堆栈帧中分配的。可以实现类似于C++ unique_ptr和shared_ptr的智能指针,但需要使用宏括号而不是{}和return,以便能够将额外的逻辑关联到作用域进入/退出。请参见https://github.com/psevon/exceptions-and-raii-in-c中的autocleanup.c实现。


0
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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接