c - 用C语言中的宏创建假函数有多少可能?

标签 c macros c99

人们总是说宏是不安全的,而且它们没有(直接)对其参数进行类型检查,等等。更糟糕的是:当发生错误时,编译器会给出固有的和难以理解的诊断,因为宏只是一团糟。


是否有可能以与函数几乎相同的方式使用宏,
进行安全的类型检查,避免典型的陷阱,并且
编译器给出正确的诊断。



我将以肯定的方式回答这个问题(自动回答)。
我想向您展示我针对该问题找到的解决方案。
将使用并遵守标准C99,以具有统一的背景。
但是(显然有一个“ but”),它将“定义”人们必须“吃掉”的某种“语法”。
这种特殊的语法旨在使编写起来最简单,也易于理解和/或处理,从而最大程度地减少不良程序的风险,更重要的是,从编译器获取正确的诊断消息。
最后,它将研究两种情况:“非返回值”宏(简单情况)和“返回值”宏(不简单,但更有趣的情况)。


让我们快速记住宏所产生的一些典型陷阱。

例子1

#define SQUARE(X) X*X
int i = SQUARE(1+5);


i的预期值:36。
i的真实值:11(带有宏扩展:1+5*1+5)。陷阱!

(典型)解决方案(示例2)

#define SQUARE(X) (X)*(X)
int i = (int) SQUARE(3.9);


i的预期值:15。
i的真实值:11(在宏扩展后:(int) (3.9)*(3.9))。陷阱!

(典型)解决方案(示例3)

#define SQUARE(X) ((X)*(X))


它适用于整数和浮点数,但很容易被破坏:

int x = 2;
int i = SQUARE(++x);


i的预期值:9(因为(2+1)*(2+1) ...)。
i的真实值:12(宏扩展:((++x)*(++x)),给出3*4)。陷阱!

可以在这里找到一种用于宏类型检查的好方法:


How to verify a type in a C macro? (by J. Gustedt)


但是我想要更多:某种接口或“标准”语法,以及(少量)易于记忆的规则。
目的是“能够使用(不实现)”宏,使其尽可能类似于函数。
这意味着:编写良好的伪函数。

为什么以某种方式有趣?

我认为这是用C语言实现的有趣挑战。

它有用吗?

编辑:在标准C中不可能定义嵌套函数。但是,有时候,人们希望能够定义嵌套在其他函数中的短(inline)函数。因此,可能需要考虑类似函数的原型宏。

最佳答案

该答案分为4个部分:


块宏的建议解决方案。
该解决方案的简短摘要。
讨论了宏原型语法。
针对类函数宏的建议解决方案。
(重要更新:)破坏了我的代码。


(1.)第一种情况。块宏(或非返回值宏)

让我们首先考虑简单的例子。假设我们需要一个“命令”
打印整数的平方,后跟“ \ n”。
我们决定使用宏来实现它。
但是我们希望参数由编译器作为int进行验证。
我们写:

#define PRINTINT_SQUARE(X) {    \
   int x = (X);              \
   printf("%d\n", x*x);      \
}



围绕(X)的括号避免了几乎所有陷阱。
此外,括号可帮助编译器正确诊断语法错误。
宏参数X在宏内部仅被调用一次。这避免了问题示例3的陷阱。
X的值立即保存在变量x中。
在宏的其余部分,我们使用变量x代替X
[重要更新:](此代码可能被破坏:请参阅第5节)。


如果我们将这一学科系统化,则可以避免宏的典型问题。
现在,类似这样的东西可以正确打印9:

int i = 3;
PRINTINT_SQUARE(i++);  


显然,这种方法可能有一个弱点:宏内定义的变量x可能与程序中也称为x的其他变量发生冲突。这是一个范围问题。但是,这不是问题,因为宏主体已被编写为{ }包围的块。这足以处理所有作用域问题,并且解决了“内部”变量x的每个潜在问题。

可以说变量x是一个额外的对象,可能是不希望的。
但是x具有(仅)临时持续时间:它在宏的开头创建,以{开头,并在宏的末尾删除,并以}结尾。
这样,x用作函数参数:创建一个临时变量以保存参数的值,并在宏“返回”时最终将其丢弃。
我们没有犯任何功能尚未完成的罪过!

更重要的是:当程序员尝试用错误的参数“调用”宏时,编译器给出的诊断与函数在相同情况下给出的诊断相同。

因此,似乎每个宏观陷阱都已解决!

但是,我们有一个语法上的问题,如您在此处看到的:


C multi-line macro: do/while(0) vs scope block
Why use apparently meaningless do-while and if-else statements in macros?
do ... while (0) macro substitutions


因此,当务之急(我说)是将do {} while(0)构造添加到类似块的宏定义中:

#define PRINTINT_SQUARE(X) do {    \
   int x = (X);              \
   printf("%d\n", x*x);      \
} while(0)


现在,这些do { } while(0)东西可以正常工作,但它是反美学的。
问题在于它对程序员没有直观的意义。
我建议使用有意义的方法,例如:

#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define PRINTINT_SQUARE(X)        \
  xxbeg_macroblock             \
       int x = (X);            \
       printf("%d\n", x*x);    \
  xxend_macroblock


(在}中包含xxend_macroblock可以避免与while(0)产生歧义)。
当然,此语法已不再安全。
必须仔细记录该文件,以避免滥用。
考虑以下丑陋的例子:

{ xxend_macroblock printf("Hello");


(2.)总结

如果我们遵循规范的样式来编写不返回值的块定义宏,则它们的行为类似于函数:

#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)

#define MY_BLOCK_MACRO(Par1, Par2, ..., ParN)     \
  xxbeg_macroblock                         \
       desired_type1 temp_var1 = (Par1);   \
       desired_type2 temp_var2 = (Par2);   \
       /*   ...        ...         ...  */ \
       desired_typeN temp_varN = (ParN);   \
       /* (do stuff with objects temp_var1, ..., temp_varN); */ \
  xxend_macroblock



对宏MY_BLOCK_MACRO()的调用是语句,而不是表达式:没有任何类型的“返回”值,甚至没有void
宏参数必须在宏开始处仅使用一次,然后使用块范围将其值传递给实际的临时变量。在宏的其余部分,只能使用这些变量。


(3.)我们可以为宏的参数提供一个接口吗?

尽管我们解决了参数类型检查的问题,
程序员无法弄清楚参数“具有”的类型。
有必要提供某种宏原型!
这是可能的,而且非常安全,但是我们必须容忍一些棘手的语法和一些限制。

您能弄清楚以下几行吗?

xxMacroPrototype(PrintData, int x; float y; char *z; int n; );
#define PrintData(X, Y, Z, N) { \
    PrintData data = { .x = (X), .y = (Y), .z = (Z), .n = (N) }; \
    printf("%d %g %s %d\n", data.x, data.y, data.z, data.n); \
  }
PrintData(1, 3.14, "Hello", 4);



第一行“定义”宏PrintData的原型。
下面,声明了类似于函数的宏PrintData。
第三行声明一个时间变量data,该变量立即收集宏的所有参数。
这一步需要程序员小心地手动编写...但这是一种简单的语法,并且编译器(至少)拒绝分配给错误类型的临时变量的参数。
(但是,编译器将对“反向”分配.x = (N), .n = (X)保持沉默)。


为了声明原型,我们用两个参数编写xxMacroPrototype


宏的名称。
宏内部将使用的“本地”变量的类型和名称的列表。我们将调用以下项目:宏的伪参数。


伪参数列表必须编写为类型变量对列表,并用分号(;)分隔(并结束)。
在宏的主体中,第一条语句将是这种形式的声明:
MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }
在宏内部,伪参数作为foo.pesudoparam1foo.pseudoparam2等调用。



xxMacroPrototype()的定义如下:

#define xxMacroPrototype(NAME, ARGS) typedef struct { ARGS } NAME


很简单,不是吗?


伪参数实现为typedef struct
可以保证ARGS是构造良好的类型标识符对的列表。
可以保证编译器将给出易于理解的诊断。
伪参数列表与struct声明具有相同的限制。 (例如,可变大小的数组只能位于列表的末尾)。
(特别是,建议使用指针指向而不是可变大小的数组声明符作为伪参数。)
不能保证NAME是真实的宏名(但是这个事实不太相关)。
重要的是,我们知道已经定义了一些“结构类型”,并与宏的参数列表相关联。
不能保证ARGS提供的伪参数列表实际上与真实宏的参数列表重合。
不能保证程序员会在宏中正确使用它。
struct-type声明的范围与xxMacroPrototype调用的作用域相同。
建议的做法是立即将宏原型与相应的宏定义放在一起。


但是,使用这种声明很容易受到约束,并且程序员很容易遵守规则。

宏可以“返回”值吗?

是。实际上,它可以检索任意数量的值,
通过简单地通过引用传递参数,就像scanf()一样。

但是您可能正在考虑其他事情:

(4.)第二种情况。类函数宏

对于它们,我们需要一种稍微不同的方法来声明宏原型,该宏原型包括用于返回值的类型。此外,我们还必须学习一种(非硬性的)技术,该技术可以使我们保持块宏的安全性,并具有我们想要的类型的返回值。

可以对参数进行类型检查,如下所示:


How to verify a type in a C macro


在宏块中,我们可以在宏本身内部声明结构变量NAME
因此将其隐藏在程序的其余部分。对于类似函数的宏,此操作无法完成(在标准C99中)。在任何宏调用之前,我们必须定义一个NAME类型的变量。如果我们准备为此付出代价,那么我们可以赚取所需的“类似于安全功能的宏”,并返回特定类型的值。
我们通过示例显示代码,然后对其进行注释:

#define xxFuncMacroPrototype(RETTYPE, MACRODATA, ARGS) typedef struct { RETTYPE xxmacro__ret__; ARGS } MACRODATA

xxFuncMacroPrototype(float, xxSUM_data, int x; float y; );
xxSUM_data xxsum;
#define SUM(X, Y) ( xxsum = (xxSUM_data){ .x = (X), .y = (Y) }, \
    xxsum.xxmacro__ret__ = xxsum.x + xxsum.y, \
    xxsum.xxmacro__ret__)

printf("%g\n", SUM(1, 2.2));


第一行定义了功能宏原型的“语法”。
这样的原型包含3个参数:


“返回”值的类型。
用于保存伪参数的“ typedef struct”的名称。
伪参数列表,以分号(;)分隔(并结束)。


“返回”值是结构中的一个附加字段,具有固定名称:xxmacro__ret__
为了安全起见,将其声明为该结构中的第一个元素。然后“粘贴”伪参数列表。

当我们使用此接口时(如果您这样称呼我),我们必须遵循一系列规则,以便:


编写一个原型声明,为xxFuncMacroPrototype()提供3个参数(示例的第二行)。
第二个参数是宏本身构建的typedef struct的名称,因此您不必担心,只需使用它即可(在示例中,此类型为xxSUM_data)。
定义一个变量,其类型就是该struct-type(在示例中:xxSUM_data xxsum;)。
使用适当数量的参数定义所需的宏:#define SUM(X, Y)
宏的主体必须用括号( )包围,以获得表达式(因此为“返回”值)。
在此括号内,我们可以使用逗号运算符(,)分隔一长串操作和函数调用。
我们需要的第一个操作是将宏SUM(X,Y)的参数X,Y“传递”到全局变量xxsum。这是通过以下方式完成的:


xxsum = (xxSUM_data){ .x = (X), .y = (Y) },

请注意,借助于C99语法提供的复合文字,正在空中创建xxSUM_data类型的对象。为了安全起见,只需读取一次宏的参数X,Y并用括号括起来,即可填充该对象的字段。
然后,我们评估表达式和函数的列表,所有表达式和函数均用逗号运算符(,)分隔。
最后,在最后一个逗号之后,我们只写xxsum.xxmacro__ret__,它被视为逗号表达式中的最后一项,因此是宏的“返回”值。

为什么所有这些东西?为什么是typedef struct
使用结构胜过使用单个变量,
因为信息全部封装在一个对象中,而数据对程序的其余部分保持隐藏。
我们不想定义“很多变量”来保存程序中每个宏的参数。
相反,通过系统地定义与宏关联的typedef struct,我们可以更轻松地处理此类宏。

我们可以避免上面的“外部变量” xxsum吗?
由于复合文字是左值,因此可以相信这是可能的。
实际上,我们可以定义这种宏,如下所示:


How to verify a type in a C macro


但是在实践中,我找不到安全地实施它的方法。
例如,上面的宏SUM(X,Y)不能仅使用此方法实现。
(我尝试使用指针到结构+复合文字来制作一些技巧,但似乎是不可能的)。

更新:

(5.)破坏了我的代码。

第1节中给出的示例可以这样破坏(如Chris Dodd在下面的评论中向我展示的那样):

int x = 5;          /* x defined outside the macro */
PRINTINT_SQUARE(x);


由于在宏内部有另一个名为x的对象(这是int x = (X);,其中X是宏PRINTINT_SQUARE(X)的形式参数),因此作为参数实际“传递”的不是在外部定义的“值” 5宏,但另一个宏:垃圾值。
为了理解它,让我们在宏扩展之后展开上面的两行:

int x = 5;
{ int x = (x); printf("%d", x*x); }


块中的变量x初始化为自己的不确定值!
通常,可以以类似的方式破坏在第1至3节中为块宏开发的技术,而我们用来保存参数的struct对象是在块内部声明的。

这表明这种代码可以被破坏,因此是不安全的:


不要尝试在宏内部声明“局部”变量来保存
参数。



有“解决方案”吗?我回答“是”:我认为,为了避免在块宏(如第1至3节中所述)的情况下发生此问题,我们必须重复对类函数宏所做的操作,即:声明保持参数在宏的外部,紧靠xxMacroPrototype()行。


这不太雄心勃勃,但是无论如何它回答了一个问题:“有多少可能...?”。另一方面,对于这两种情况,我们现在采用相同的方法:块宏和类似于函数的宏。

关于c - 用C语言中的宏创建假函数有多少可能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18424458/

相关文章:

macros - 在 defmacro 中使用标签+宏进行正确的反引用和变量捕获

C 在调用 execvp 之前测试文件是否存在

c - Cygwin 中的段错误和默认变量初始化

gcc - 变量值的字符串化

c - 宏 hell : Platform-independent pointer to setjmp/sigsetjmp

c - C99 中的结构与 ANSI-C 有何区别?

c - 为什么有些 C 标准头文件以 'std' 开头,而有些则不是?

c - struct 中的匿名 union 不在 c99 中?

c - 简单的堆栈,弹出

c - strtol 不从读取(fd、缓冲区、ssize_t)转换字符串