有没有一种方法可以保存带有参数的函数调用?

4

我正在进行内存管理方面的实验,并尝试创建一些有助于这方面工作的东西。目前,我在想是否有办法在 C 语言中重复 Go 中的 'defer' 功能。

以下是不熟悉 'defer' 的人的快速示例:

package main

import "fmt"

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    return
}

将会打印

3
2
1

我在考虑一些宏,可以将带有参数的函数推入某个堆栈,并在调用函数退出时调用它们。就像这样:

int func(void)
{
    MEMSTACK_INIT;

    char * string = NULL;
    node_t * node = NULL;
    MEMSTACK_PUSH(free(string));
    MEMSTACK_PUSH(NodeFree(&node));

    <..>

    switch (something)
    {
    case ONE : RETURN ERROR_ONE;
    case TWO : RETURN ERROR_TWO;
    case THR :
        switch (something else)
        {
            <.. Many more code ..>
        }
    }        

    RETURN ERROR_GOOD;
}

除了编写自己的预处理程序之外,是否有一种方法可以在某个地方存储带有参数的函数调用呢?换句话说,我想要将先前的代码预处理成以下内容:

int func(void)
{
    <.. Some MEMSTACK initialisation stuff (if needed) ..>

    char * string = NULL;
    node_t * node = NULL;

    <..>

    switch (something)
    {
    case ONE :             
        free(string);
        NodeFree(&node);
        return ERROR_ONE;
    case TWO :             
        free(string);
        NodeFree(&node);
        return ERROR_TWO;
    case THR :
        switch (something else)
        {
            <.. Many more code ..>
        }
    }        

    free(string);
    NodeFree(&node);
    return ERROR_GOOD;
}

对于需要在退出之前进行大量清理的功能来说,使用goto cleanup技巧确实是一个好方法。


1
这似乎是一个有趣的问题,但不幸的是很难理解...你脑海中有很多上下文,但应该在这里呈现。 - Eugene Sh.
1
我不了解Go语言,但延迟执行并不仅仅是使用堆栈来反转执行顺序,它还有更多的用途。不要试图在C语言中使用这种技术,这只会使你的代码变得混乱,而且没有语言支持会极大地增加调试和维护的难度。此外,请注意,堆栈操作的成本可能比直接执行更高(这也是延迟执行的主要思想)。 - too honest for this site
你可以复制函数对象的概念:将数据和方法绑定到一个结构体中。 - user3528438
1
дҪ жҳҜдёҚжҳҜдёҚе–ңж¬ўatexitеҮҪж•°пјҹжҲ–иҖ…constructorе’ҢdestructorеҮҪж•°еұһжҖ§з”ЁдәҺmainеҮҪж•°пјҹ - David C. Rankin
1
@hacatu 如果你感兴趣的话:libffcall中的avcall试图隐藏调用约定的差异。 - Daniel Jour
显示剩余11条评论
1个回答

2
我正在尝试进行内存管理实验,并尝试创建任何形式的有助于此的东西。
一个好的方法是在任何函数中仅具有一个return。可能会标记为一个标签(是的,可以使用goto,但这通常也不被鼓励)。当然:始终确保知道谁拥有分配的内存以及何时(以及在哪里)所有权已转移!
现在,让我们……重复Go中的“defer”功能,在C中实现它。
首先,为了推迟调用,我们需要存储函数(指向它的指针)以及评估的参数。由于C是静态类型的,我们需要将其统一为单个类型:
struct Fn {
  void * parameters; // pointer to memory where the parameters are stored
  void (*function)(void *); // pointer to function able to unpack parameters from above
  struct Fn * next; // we want a stack, so ...
};

对于我们最终要推迟的每个函数,我们需要一种存储它的参数的方式。因此,我们定义了一个能够容纳参数的结构体和一个能够从该结构体中解包参数的函数:
#define MAKE_DEFERRABLE(name, N, ...) \
  struct deferred_ ## name ## _parameters { PARAMS(N, __VA_ARGS__) }; \
  void deferred_ ## name (void * p) { \
    struct deferred_ ## name ## _parameters * parameters = p; \
    printf(" -- Calling deferred " #name "\n"); \
    (void)name(CPARAMS(N)); \
  }

N是参数的数量。有一些技巧可以从__VA_ARGS__中推断出来,但我会把它留给读者作为练习。该宏包含另外两个宏扩展,PARAMSCPARAMS。前者扩展成适合定义struct内容的列表。后者扩展成提取struct成员作为参数的代码:

#define PARAM_0(...)
#define PARAM_1(type, ...) type p1; PARAM_0(__VA_ARGS__)
#define PARAM_2(type, ...) type p2; PARAM_1(__VA_ARGS__)
#define PARAM_3(type, ...) type p3; PARAM_2(__VA_ARGS__)
#define PARAM_4(type, ...) type p4; PARAM_3(__VA_ARGS__)
#define PARAMS(N, ...) SPLICE(PARAM_, N)(__VA_ARGS__)

#define CPARAM_0 
#define CPARAM_1 parameters->p1
#define CPARAM_2 parameters->p2, CPARAM_1
#define CPARAM_3 parameters->p3, CPARAM_2
#define CPARAM_4 parameters->p4, CPARAM_3
#define CPARAMS(N) SPLICE(CPARAM_, N)

如果我们想要推迟具有超过4个参数的函数,则需要进行调整。 SPLICE是一个不错的小助手:
#define SPLICE_2(l,r) l##r
#define SPLICE_1(l,r) SPLICE_2(l,r)
#define SPLICE(l,r) SPLICE_1(l,r)

接下来,我们需要以某种方式存储延迟函数。为了简单起见,我选择动态分配它们并保持对最近一个的全局指针:
struct Fn * deferred_fns = NULL;

显然,您可以在许多方面进行扩展:使用(有限的)静态存储、使其成为线程本地的、使用每个函数的deferred_fns、使用alloca等。
但是这里提供的是简单的、不适用于生产环境的版本(缺少错误检查)。
#define DEFER(name, N, ...) \
  do { \
    printf(" -- Deferring a call to " #name "\n"); \
    if (deferred_fns == NULL) { \
      deferred_fns = malloc(sizeof(*deferred_fns)); \
      deferred_fns->next = NULL; \
    } else { \
      struct Fn * f = malloc(sizeof(*f)); \
      f->next = deferred_fns; \
      deferred_fns = f; \
    } \
    deferred_fns->function = &(deferred_ ## name); \
    struct deferred_ ## name ##_parameters * parameters = malloc(sizeof(*parameters)); \
    SPARAMS(N,__VA_ARGS__); \
    deferred_fns->parameters = parameters; \
  } while(0)

这段代码创建了一个新的struct Fn,将其置于栈顶(以单向链表deferred_fns的形式),并相应地设置其成员。重要的SPARAMS将参数保存到相应的struct中:
#define SPARAM_0(...)
#define SPARAM_1(value, ...) parameters->p1 = (value); SPARAM_0(__VA_ARGS__)
#define SPARAM_2(value, ...) parameters->p2 = (value); SPARAM_1(__VA_ARGS__)
#define SPARAM_3(value, ...) parameters->p3 = (value); SPARAM_2(__VA_ARGS__)
#define SPARAM_4(value, ...) parameters->p4 = (value); SPARAM_3(__VA_ARGS__)
#define SPARAMS(N, ...) SPLICE(SPARAM_, N)(__VA_ARGS__)

注意:通过让参数从后往前进行评估,可以解决参数评估顺序的问题。C语言并没有强制规定评估顺序。
最后,还有一种方便的方法来运行这些延迟函数。
void run_deferred_fns(void) {
  while (deferred_fns != NULL) {
    deferred_fns->function(deferred_fns->parameters);
    free(deferred_fns->parameters);
    struct Fn * bye = deferred_fns;
    deferred_fns = deferred_fns->next;
    free(bye);
  }
}

一个小测试

void foo(int x) {
    printf("foo: %d\n", x);
}
void bar(void) {
    puts("bar");
}
void baz(int x, double y) {
    printf("baz: %d %f\n", x, y);
}
MAKE_DEFERRABLE(foo, 1, int);
MAKE_DEFERRABLE(bar, 0);
MAKE_DEFERRABLE(baz, 2, int, double);

int main(void) {
  DEFER(foo, 1, 42);
  DEFER(bar, 0);
  DEFER(foo, 1, 21);
  DEFER(baz, 2, 42, 3.14);
  run_deferred_fns();
  return 0;
}

为了实现与您示例中相同的行为,将deferred_fns设为本地变量,并将其作为参数传递给run_deferred_fns。使用简单的宏进行包装,完成:
#define PREPARE_DEFERRED_FNS struct Fn * deferred_fns = NULL;
#define RETURN(x) do { run_deferred_fns(deferred_fns); return (x); } while (0)

欢迎来到疯狂世界。
注:我的解决方案在“源代码级别”上运行。这意味着您需要在源代码中指定可延迟的函数。这意味着您不能通过dlopen加载的函数进行延迟。如果您愿意,还有一种不同的方法,即在ABI级别上工作:avcall,它是libffcall的一部分。
现在,我真的需要我的括号...很多括号(())))(()(((()

谢谢你的好回答!我认为我们甚至可以将MAKE_DEFERRABLE移动到函数定义中,像这样:DEFERABLE(void, foo(int x, double b)) { ... }。我曾经在一些好的库(比如libxml/fontconfig)中看到过类似的宏定义函数的情况。 - Alex Tiger
1
是的,可以添加这样一个宏。拥有一个MAKE_DEFERRABLE宏的好处是可以使所有已知静态函数都能够被延迟执行。例如fcloseprintf(只针对一种调用方式)和其他库函数 :) - Daniel Jour
抱歉,上一条评论编辑已经太晚了,我会新建一条。我认为更好的返回方式是:#define RETURN run_deferred_fns(); return,因为这样我们可以写RETURN 0;或者RETURN ERROR_CODE;而不会破坏基本的返回语法。更新:我将尝试删除MAKE_DEFERRABLE宏,我认为我们可以创建通用的“即时调用”。 - Alex Tiger
1
不,你应该像我的答案一样。否则想想 if (condition) RETURN 42; 会发生什么。 - Daniel Jour
是的,我没有考虑到这种情况。抱歉。 - Alex Tiger

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