人们总是说宏是不安全的,而且它们没有(直接)对其参数进行类型检查,等等。更糟糕的是:当发生错误时,编译器会给出固有的和难以理解的诊断,因为宏只是一团糟。
是否有可能以与函数几乎相同的方式使用宏,
进行安全的类型检查,避免典型的陷阱,并且
编译器给出正确的诊断。
我将以肯定的方式回答这个问题(自动回答)。
我想向您展示我针对该问题找到的解决方案。
将使用并遵守标准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.pesudoparam1
,foo.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/