Golang有一个名为defer的有用的语言 struct ,它允许用户推迟函数的执行,直到周围的函数返回.这对于确保安全地销毁资源,同时保持创建-销毁逻辑紧密相连非常有用.

我对在C99中通过宏实现这一点很感兴趣.以下是我到目前为止编写的内容,以及一些示例代码以显示宏的实际操作:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <math.h>

void* log_malloc(size_t size) {
    void* ptr = malloc(size);
    printf("Allocated %zu bytes: %p\n", size, ptr);
    return ptr;
}

void log_free(void* ptr) {
    free(ptr);
    printf("Freed %p\n", ptr);
}

typedef void (*DeferFunc)();
typedef struct Defer {
    DeferFunc f;
    void* p;
} Defer;

#define PRE_DEFER() \
    size_t num_defers = 0; \
    Defer defers[32];

#define DEFER(func, param) \
    defers[num_defers].f = func;\
    defers[num_defers].p = param;\
    num_defers++;

#define POST_DEFER() \
    while(num_defers > 0) { \
        defers[num_defers - 1].f(defers[num_defers - 1].p); \
        num_defers--; \
    }

void test() {
    PRE_DEFER();
    char* a = log_malloc(10);
    DEFER(log_free, a);
    char* b = log_malloc(20);
    DEFER(log_free, b);
    char* c = log_malloc(30);
    DEFER(log_free, c);
    POST_DEFER();
}

int main() {
    test();
    return 0;
}

这段代码可以工作,在我的基本基准测试中,与仅手动对析构函数语句进行排序相比,它的性能开销约为10%.但是,它受到可处理的DEFER个宏调用的数量限制.当然,我可以传入我希望在PRE_DEFER宏中执行的延迟次数,但这样做可能会很繁琐,而且容易出错,特别是在存在分支逻辑或包含DEFER条语句的循环的情况下.有没有办法以编程方式在生成的宏代码中填充defers数组的大小(作为VLA)?

我已经考虑过或不感兴趣的解决方案:

  • 只需使用C++.这一问题可以与Raii解决.然而,析构函数逻辑变得隐含,我对C++提供的额外复杂性/特性爬行不感兴趣.
  • 使用编译器扩展.GCC(我相信还有clang)有一个可以滥用的__attribute__ cleanup语法,执行得很好,但在MSVC中不受支持,并且需要修改提供的析构函数来使用指向变量的指针,而不是变量本身.
  • 在每个DEFER处使用重复的alloca个呼叫.这确实有效,但速度很慢(比手动排序函数调用或我当前的defer解决方案慢2倍).

任何帮助都将不胜感激.


Update:

感谢@DAI指出MSVC的__try__finally编译器构造.使用它们,我得到了以下代码:

#define DEFER(fini_call, body) \
    __try{ body } \
    __finally{ fini_call; }

void test() {
    char* a = log_malloc(10);
    DEFER(log_free(a), {
        char* b = log_malloc(20);
        DEFER(log_free(b), {
            char* c = log_malloc(30);
            DEFER(log_free(c), {})
        })
    });
}

这样就完成了工作,省go 了对PRE_DEFERPOST_DEFER个宏的需求.但是,它与我的原始宏的语法不兼容.此外,它还导致了我的原始版本中隐藏的嵌套,因为函数的其余部分需要传递到DEFER宏中.

为了完整起见,我包含了代码的GCC/clang属性((清理))版本.具体情况如下:

#define DEFER(type, name, init_func, fini_func_name) \
    type name __attribute__ ((__cleanup__(fini_func_name))) = init_func

void* log_malloc(size_t size) {
    void* ptr = malloc(size);
    printf("Allocated %zu bytes: %p\n", size, ptr);
    return ptr;
}

void log_free(char** ptr) {
    free(*ptr);
    printf("Freed %p\n", *ptr);
}

int test() {
    DEFER(char*, a, log_malloc(10), log_free);
    DEFER(char*, b, log_malloc(20), log_free);
    DEFER(char*, c, log_malloc(30), log_free);
    return 0;
}

这当然需要宏的另一种语法,该语法与原始版本和MSVC扩展版本都不兼容.它还需要一种笨拙的语法来包装和拆分创建函数调用.

理想的解决方案是将所有这些版本(普通的C和各种编译器扩展)协调到一个跨平台的单一语法中.

推荐答案

性能开销约为10%.

这很可能是因为间接调用清理函数造成的.编译器既不能优化间接调用,也不能定向调用.

它受可处理的DEFER个宏调用次数的限制.

是也不是.您的实现对每个PRE_DEFER的调用次数有限制,但您可以通过将多个PRE_DEFER嵌套在块中来将多个PRE_DEFER放入同一函数中.或者 for each 变量提供一个区别标签,然后使用该标签形成包含所需信息的变量的名称.当然,每个PRE_DEFER也需要自己的POST_DEFER个.

有没有办法以编程方式在生成的宏代码中填充延迟数组的大小(作为VLA)?

不是在DEFER个电话出现之前,不是.

你可以考虑更像这样的事情:

#define DEFER(cleanup) for (_Bool done_ = 0; !done_; (cleanup), done_ = 1)

这会将参数给出的表达式的求值推迟到DEFER,直到下一条语句完成后,下一条语句可以是复合语句,但不一定是复合语句.您将使用它与您的__try/__finally示例类似,但是语法不那么复杂:

void test() {
    char* a = log_malloc(10);
    DEFER(log_free(a)) {
        char* b = log_malloc(20);
        DEFER(log_free(b)) {
            char* c = log_malloc(30);
            DEFER(log_free(c));
        }
    }
}

我不完全确定你说的是什么意思.

理想的解决方案是将所有这些版本(普通的C和各种编译器扩展)协调到一个跨平台的单一语法中.

...但上述功能仅使用标准的C99功能.它可以与任何C99或更高版本的实现一起使用.


荣誉奖:pthread_cleanup_push() and pthread_cleanup_pop()人.这些不仅是标准的C语言,而且本身也是标准化的--而且是通过POSIX,而不是通过C语言.它们将与p线程实现一起工作,但不会更广泛.

C++相关问答推荐

在x86汇编中,为什么当分子来自RDRAND时DIV会引发异常?

try 使用sigqueue函数将指向 struct 体的指针数据传递到信号处理程序,使用siginfo_t struct 体从一个进程传递到另一个进程

我应该如何解决我自己为iOS编译的xmlsec1库的问题?转换Ctx.first在xmlSecTransformCtxPrepare()之后为空

核心转储文件中出现奇怪的大小变化

#If指令中未定义宏?

使用C时,Windows CMD中的argc参数是否包含重定向命令?

每次除以或乘以整数都会得到0.0000

在句子中转换单词的问题

为什么数组的最后一个元素丢失了?

不同出处的指针可以相等吗?

';\n&39;和';\r&39;中的';\n&39;之间有什么关系?

C语言中神秘的(我认为)缓冲区溢出

C程序向服务器发送TCPRST

我可以创建适用于不同endian的 colored颜色 struct 吗?

基于蝶数恰好有8个除数的事实的代码

在链表中插入一个值

如何向 execl 创建的后台程序提供输入?

GDB 用内容初始化数组

C99 的 %zu 格式说明符不起作用

如何在 C 中编辑 struct 体中的多个变量