如何在 C 中实现 Go 的 defer() 以便允许声明变量?

psp*_*int 3 c gcc c-preprocessor c11 gcc-extensions

在这个现代 C视频中,有一个技巧可以推迟代码的执行,直到块/作用域退出。它的使用方法如下:

\n
int main()\n{\n    int foo=0, bar;\n    const char *etc = "Some code before defer";\n\n    defer(profile_begin(), profile_end())\n    {\n        /* Some code, which will be automatically \n         * preceded by call to profile_begin() and\n         * followed by run of profile_end().*/\n         foo++;\n         bar = 1;\n    }\n\n    etc = "Some code after defer";\n    foo = bar + 1;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

视频中的实现:

\n
#define macro_var_line(name) concat(name, __LINE__)\n#define defer(start,end) for( \\\n        int macro_var_line(done) = (start,0); \\\n        !macro_var_line(done); \\\n        (macro_var_line(done) += 1), end)\n
Run Code Online (Sandbox Code Playgroud)\n

它的实现非常简单。可能令人困惑的是macro_var_line(name)宏。其目的是通过向临时变量添加当前行号(调用 defer 的位置)来简单地确保临时变量具有唯一的“混淆”名称。

\n

然而问题是,无法将代码传递到声明新变量的代码片段,因为它被粘贴到使用类型(the )start的 for() 逗号运算符中。所以这是不可能的,例如:intint macro_var_line(done) = \xe2\x80\xa6

\n
defer(FILE *f = fopen("log.txt","a+"), fclose(f))\n{\n    fprintf(f,"Some message, f=%p",f);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我想要有这样的宏,能够在start代码片段中声明新的变量。它可以通过标准 C99、C11 或者某些 GCC 扩展来实现吗?

\n

更新:我找到了一个利用 GCC 嵌套函数的解决方案。基本上,{ bblock }宏后面的那个defer()成为嵌套函数体。并且可以转发声明嵌套函数并从块之前调用它,即:

\n
#define defer(start,end) \\\n                auto void var_line(routine) (void); \\\n                start; \\\n                /* Invoke above predeclared void routine_123(void) function */ \\\n                var_line(routine)(); \\\n                end; \\\n                /* Define the nested function */ \\                    \n                void var_line(routine) (void)\n
Run Code Online (Sandbox Code Playgroud)\n

UPDATE2:这是一个优雅的版本:

\n
    \n
  1. 运行第一个前导语句作为开始,最后一个作为结束代码,
  2. \n
  3. for()在自己的/声明空间中运行第一个语句,
  4. \n
  5. if(cond == 0)通过检查/块启动正确运行块。
  6. \n
\n
\n
#define defer(...) \\\n    for (int var_line(cond) = 0; var_line(cond) == 0; ) \\\n        for (FIRST_ARG(__VA_ARGS__); var_line(cond) == 0; ) \\\n            for (SKIP_LAST_ARG(SKIP_FIRST_ARG(__VA_ARGS__)); \\\n                var_line(cond) == 0; \\\n                    var_line(cond) += 1 ) \\\n                for (int var_line(cond_int) = 0; \\\n                    var_line(cond_int) <= 1; \\\n                        var_line(cond_int) += 1 ) \\\n                    if (var_line(cond_int) == 1) \\\n                    { \\\n                        LAST_ARG(__VA_ARGS__); \\\n                    } else if (var_line(cond_int) == 0)\n
Run Code Online (Sandbox Code Playgroud)\n

Joh*_*ger 6

正如我在评论中所表达的,我的建议是首先避免使用这样的东西。无论您的视频说了或暗示了什么,现代 C 程序员的普遍观点是应尽量减少宏的使用。类似变量的宏通常应该表示与上下文无关的常量值,而类似函数的宏通常最好实现为实际函数。这并不是说必须避免所有宏的使用,但大多数现代 C 专业人士对复杂的宏的看法很差,而您的宏defer()足够复杂以符合资格。

此外,尝试将其他语言的风格和习语导入 C 语言对自己没有任何好处。每种语言的通用习语得以建立是因为它们适用于该语言,而不是通常因为它们具有固有的内在价值。我建议你学习 C 和 C 程序员使用的习惯用法,而不是如何编写看起来像 Go 的 C 代码。

话虽如此,让我们考虑一下您的defer()宏。你写,

然而问题是无法将代码传递到声明新变量的开始片段

,但实际上限制比这更强。由于宏start在逗号表达式 ( start,0) 中使用参数,因此它本身必须是表达式。不允许任何类型的声明或完整陈述。这仅与出现在for语句控制块的第一个子句中的表达式间接相关。(这同样适用于end论证。)

还需要注意的是,如果关联语句的执行通过or语句end分支出块或通过执行不返回的函数(例如or )而终止,则宏扩展为无法计算表达式的代码。此外,与 Go 不同的是,表达式在提供的语句之后进行完整计算——之前没有计算任何部分,这可能会让 Go 程序员感到惊讶。这些也是下面介绍的选项的特征。returngotoexit()longjmp()deferend

如果您只想传递startend作为宏参数,并且希望允许声明出现在 中start,那么您可以这样做:

// Option 1
#define defer(start,end) start; for( \
        int macro_var_line(done) = 0; \
        !done; \
        (macro_var_line(done) += 1), (end))
Run Code Online (Sandbox Code Playgroud)

这会start移出for宏替换文本中的语句,移动到可能出现任意 C 代码的位置。但请注意,任何变量声明的作用域都将限定在最里面的包含块。

如果您想限制声明的范围,那么还有这种替代方案及其变体,我发现它比原来的更简单:

// Option 2
#define defer(start, end, body) { start; body end; }
Run Code Online (Sandbox Code Playgroud)

你可以像这样使用它:

defer(FILE *f = fopen("log.txt","a+"), fclose(f), // argument list continues ...
    fprintf(f,"Some message, f=%p",f);
);
Run Code Online (Sandbox Code Playgroud)

这在某种程度上适合您的特定示例,因为它假设主体作为零个或多个完整语句的序列给出(可以包括块、流控制语句)。正如您所看到的,它还要求主体作为宏参数传递,而不是出现在宏调用之后,但我认为这是一个优点,因为它有助于识别延迟代码的启动点。