【问题标题】:How much is it possible to create fake-functions with macros in C?在 C 中使用宏创建伪函数的可能性有多大?
【发布时间】:2013-08-27 18:45:44
【问题描述】:

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

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

  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) 函数。因此,可以考虑使用类似函数的原型宏。

【问题讨论】:

  • 您可能需要考虑 GCC 的扩展名 statement expressions — 或者同样,您可能不会。但是,我认为如果宏在任何地方都可用,那么语句表达式就不会被发明出来。
  • 考虑在宏中提供需要此功能的原因,而不是简单地使用内联函数。
  • @JonathanLeffler 实际上,你是对的。要了解原因,请参阅我添加到答案中的新第 5 节。那里解释了 Chris Dodd 用来破坏我的 PRINTINT_SQUARE(X) 宏的示例。
  • @tomlogic 这是一个好问题。这时我突然想到一个想法:在 GCC 中我们有嵌套函数,但在严格的 C99 标准下我们没有。有时人们会对仅在另一个函数 F1()内部 声明的内联函数 F2() 感到满意,但我们不能在“那里”定义它。但是我们可以在 F1() 的主体内定义一个宏 F2()。此外,F2() 将仅在 F1() 范围内可见。

标签: c macros c99


【解决方案1】:

这个答案分为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

(在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)保持沉默)。

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

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

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

    • 在宏的主体中,第一条语句将是这种形式的声明:
      MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }

    • 在宏内部,伪参数以foo.pesudoparam1foo.pseudoparam2 等形式调用。

xxMacroPrototype()的定义如下:

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

很简单,不是吗?

  • 伪参数实现为typedef struct
  • 保证 ARGS 是一个结构良好的类型标识符对列表。
  • 保证编译器将给出易于理解的诊断。
  • 伪参数列表具有与struct 声明相同的限制。 (例如,可变大小的数组只能位于列表的末尾)。 (特别推荐使用 pointer-to 而不是 variable-size array 声明符作为伪参数。)
  • 不保证 NAME 是一个真正的宏名(但这个事实不太相关)。
    重要的是我们知道“那里”定义了一些结构类型,与宏的参数列表相关联。
  • 不能保证 ARGS 提供的伪参数列表实际上在某种程度上与真实宏的参数列表一致。
  • 不保证程序员会在宏中正确使用它。
  • 结构类型声明的范围与完成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. 伪参数列表,以分号 (;) 分隔(并结束)。

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

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

  1. 编写一个原型声明,为 xxFuncMacroPrototype() 提供 3 个参数(示例的第 2 行)。
  2. 第二个参数是宏自己构建的typedef struct的名称,所以你不用担心,直接使用它(在例子中这个类型是xxSUM_data)。
  3. 定义一个变量,其类型只是该结构类型(在示例中:xxSUM_data xxsum;)。
  4. 使用适当数量的参数定义所需的#define SUM(X, Y)
  5. 宏的主体必须用括号( ) 括起来,才能获得表达式(因此,“返回”值)。
  6. 在这个括号内,我们可以使用逗号运算符 (,) 分隔一长串操作和函数调用。
  7. 我们需要的第一个操作是将宏 SUM(X,Y) 的参数 X、Y“传递”给全局变量 xxsum。这是由以下人员完成的:

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

观察xxSUM_data 类型的对象是在C99 语法提供的复合文字的帮助下在空中创建的。为安全起见,该对象的字段通过读取宏的参数 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 的对象(这个:int x = (X);,其中X 是宏PRINTINT_SQUARE(X) 的形参),实际上作为参数“传递”的不是“值” 5 定义在宏之外,还有一个:一个垃圾值。
为了理解它,让我们在宏展开之后展开上面的两行:

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

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

这说明这种代码是可以破解的,所以是不安全的:

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

  • 有“解决方案”吗?我回答“是”:我认为,为了避免在块宏的情况下出现这个问题(如第 1 到第 3 节所述),我们必须重复我们对类函数宏所做的事情,即:声明宏外部的holding-parameters 结构,就在xxMacroPrototype() 行之后。

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

【讨论】:

  • 虽然编译器编写者可以使用以双下划线开头的宏名称,但使用编译器的普通人不应该这样做;这些名称是为实现而保留的。因此,在普通程序员(而不是编译器编写者)可以安全使用之前,上述所有双下划线宏都应该重命名为不带双下划线。
  • 我已经把所有的 '__' 改成了 'xx'。
  • 我重新扫描了这个答案......我有点困惑。它以 4 点议程开始,但似乎没有与这 4 点相对应的部分。您有两个块,一个用于“不返回值的宏”,另一个用于“确实返回值的宏”,它们在问题中提到。我认为您需要考虑一些 2 级 (##) 标题,并查看呈现顺序。
  • 另外,你应该知道你的PRINTF_SQUARE 宏有两个限制:(1)它只要求参数可以转换为int,(2)它不能在每个可以使用真正的void printf_square(int x); 函数的上下文(例如,在ifforwhileswitch 语句的组件中使用逗号运算符(并不是说使用这种在大多数这些地方都有功能,但是...)。我还没有关注所有链接。我还会观察到-Wshadow 嵌套x 存在问题,但这不是默认编译选项海湾合作委员会。
  • @JonathanLeffler 我已经对所有章节标题进行了编号。似乎这个小小的变化大大改善了演示的结构。感谢所有的更正。
【解决方案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) )

【讨论】:

  • 你说得对。我完全忘了提到 C11。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-18
  • 2011-05-26
  • 2011-02-23
  • 2021-12-23
相关资源
最近更新 更多