9

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

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

  1. 我将以肯定的方式回答这个问题(自动回答)。
  2. 我想向您展示我找到的解决此问题的方法。
  3. 将使用和尊重标准 C99,以具有统一的背景。
  4. 但是(显然有一个“但是”),它将“定义”某种人们必须“吃”的“语法”。
  5. 这种特殊的语法旨在成为最容易编写和/或最容易理解和/或处理的语法,最大限度地减少格式错误的程序的风险,更重要的是,从编译器获得正确的诊断消息。
  6. 最后,它将研究两种情况:“非返回值”宏(简单情况)和“返回值”宏(不简单,但更有趣的情况)。

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

示例 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)。陷阱!

在这里可以找到一个很好的宏类型检查方法:

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

为什么这在某种程度上很有趣?

我认为这是在 C 中实现的一个有趣的挑战。

有用吗?

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

4

2 回答 2

7

这个答案分为4个部分:

  1. 块宏的建议解决方案。
  2. 该解决方案的简要总结。
  3. 讨论了宏原型语法。
  4. 为类似函数的宏提出的解决方案。
  5. 重要更新:)破坏我的代码。

(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它作为函数参数工作:创建一个时间变量来保存参数的值,并在宏“返回”时最终被丢弃。我们没有犯任何功能尚未完成的罪!

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

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

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

因此,必须(我说)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

(包含}inxxend_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使用 2 个参数编写:

  1. 宏的名称。
  2. 将在宏内部使用的“局部”变量的类型和名称列表。我们将调用这个项目:宏的 伪参数。

    • 伪参数列表必须写成类型变量对的列表,用分号 (;) 分隔(并结束)。

    • 在宏的主体中,第一条语句将是这种形式的声明:
      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.) 第二种情况。类函数宏

对于他们来说,我们需要一些不同的方法来声明宏原型,其中包含返回值的类型。此外,我们必须学习一种(不难)技术,让我们保持块宏的安全性,返回值具有我们想要的类型。

可以实现参数的类型检查,如下所示:

在块宏中,我们可以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 个参数:

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

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

当我们使用这个接口时(如果你让我这样称呼它),我们必须遵循一系列规则,依次为:

  1. 编写一个原型声明,为 xxFuncMacroPrototype() 提供 3 个参数(示例的第二行)。
  2. 第二个参数是typedef struct宏本身构建的 a 的名称,所以你不用担心,直接使用它(在示例中这个类型是xxSUM_data)。
  3. 定义一个变量,其类型只是该结构类型(在示例中:)xxSUM_data xxsum;
  4. 使用适当数量的参数定义所需的#define SUM(X, Y).
  5. 宏的主体必须用括号括( )起来,以便获得一个 EXPRESSION (因此,一个“返回”值)。
  6. 在这个括号内,我们可以使用逗号运算符 (,) 分隔一长串操作和函数调用。
  7. 我们需要的第一个操作是将宏 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 吗?由于复合文字是值,因此可以相信这是可能的。
其实我们可以定义这种宏,如下图:

但在实践中,我找不到以安全方式实施它的方法。
例如,上面的宏 SUM(X,Y) 不能只用这个方法来实现。
(我试图用指向结构的指针+复合文字来做一些技巧,但这似乎是不可能的)。

更新:

(5.) 破坏我的代码。

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

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

由于在宏内部还有另一个名为 x 的对象(this: 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()行之后。

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

于 2013-08-25T00:22:31.110 回答
2

虽然像宏这样的函数的自我回答技术很聪明,但它并没有提供原始“不安全”宏的“通用性”,因为它不允许传入任意类型。而且,一旦宏被拒绝为特定类型工作,那么维护内联函数会更简单、更安全、更容易。

inline float sum_f (float x, float y) { return x + y; }

使用 C.11,您可以使用新的通用选择运算符_Generic来定义一个宏,该宏可以在给定参数类型的情况下调用适当的内联函数。类型选择表达式( 的第一个参数_Generic)用于确定类型,但不计算表达式本身。

#define SUM(X, Y) \
    _Generic ( (X)+(Y) \
             , float : sum_f(X, Y) \
             , default : sum_i(X, Y) )
于 2013-08-25T01:54:30.463 回答