【问题标题】:Why are C macros not type-safe?为什么 C 宏不是类型安全的?
【发布时间】:2013-07-31 23:50:06
【问题描述】:

如果遇到此声明 multiple times 并且无法弄清楚它应该是什么意思。由于生成的代码是使用常规 C 编译器编译的,因此最终会像任何其他代码一样多(或少)进行类型检查。

那么为什么宏不是类型安全的?这似乎是他们应该被视为邪恶的主要原因之一。

【问题讨论】:

  • 重点是C编译器不处理宏定义。预处理器处理它们
  • 嗯,是的,但是如果生成的代码实际上违反了类型系统,编译器会捕捉到它。
  • C++FAQ 详细介绍了为什么宏在这里是邪恶的:parashift.com/c++-faq-lite/inline-vs-macros.html
  • @Sarien: "...如果生成的代码实际上违反了类型系统,编译器会捕捉到它。" 你是绝对正确的。此外,我想说“宏不是类型安全的”这句话没有意义,因为没有人认为它们是类型安全的,因为它们只是 C 语言的助手,而不是 C 语言的组成部分它。
  • Broad 声称宏不是类型安全的说法来自没有考虑过的人。 #define NUM_REPS 3 是类型安全的; NUM_REPS 扩展为 3,其类型为 int。正如一些答案所暗示的那样,如果您没有考虑过它们的作用,类似函数的宏会产生令人惊讶的结果;这是否上升到不“类型安全”的程度取决于您所说的“类型安全”的确切含义。

标签: c++ c types macros


【解决方案1】:

当宏运行时,它只是通过源文件进行文本匹配。这是在任何编译之前,所以它不知道任何它改变的数据类型。

【讨论】:

  • 正如你所说,它是在编译之前完成的,这是检查类型的地方,这正是我困惑的地方。
  • 确实,代码都将通过编译器使用通常的类型系统进行编译。但是宏扩展步骤本身不尊重数据类型 - 它不能,因为它发生在编译开始之前
  • 特别是您的 MSDN 链接讨论了函数宏(如这里:stackoverflow.com/questions/163365/…),重点是 - 它识别两个输入并将它们相加,但没有像编译器那样仔细检查它们的类型做
  • 例如,我可以输入 MACRO_ADD(orange, 1.5),它会很高兴地生成“orange + 1.5”。当然,您是正确的,此代码稍后会在编译期间失败。但是它很高兴地通过了宏扩展,这就是为什么我们可以说宏扩展不是类型安全的。
【解决方案2】:

好吧,它们不是直接类型安全的...我想在某些场景/用法中,您可能会争辩说它们可以是间接(即生成的代码)类型-安全的。但是您当然可以创建一个用于整数的宏并将其传递给字符串……处理宏的预处理器当然不在乎。编译器可能会窒息,具体取决于使用情况...

【讨论】:

  • 这听起来不错,但有人应该向全世界指出,这不会以任何方式使宏不利于生成否则必须复制的可能。 :)
  • 我曾经参与过一个项目,其中一个同事在其中一个核心头文件中 #define'd printf 是别的东西。
【解决方案3】:

考虑典型的“max”宏与函数:

#define MAX(a,b) a < b ? a : b
int max(int a, int b) {return a < b ? a : b;}

当人们说宏在函数的方式上不是类型安全的时,这就是人们的意思:

如果函数的调用者写入

char *foo = max("abc","def");

编译器会发出警告。

然而,如果宏的调用者写道:

char *foo = MAX("abc", "def");

预处理器会将其替换为:

char *foo = "abc" < "def" ? "abc" : "def";

这将毫无问题地编译,但几乎可以肯定不会给出您想要的结果。

另外当然副作用不同,考虑函数情况:

int x = 1, y = 2;
int a = max(x++,y++); 

max() 函数将对 x 和 y 的原始值进行操作,并且后增量将在函数返回后生效。

在宏观情况下:

int x = 1, y = 2;
int b = MAX(x++,y++);

第二行被预处理以给出:

int b = x++ < y++ ? x++ : y++;

同样,没有编译器警告或错误,但不会是您预期的行为。

【讨论】:

  • 在这个例子中,模板也不是类型安全的。模板化的max 函数将允许完全相同的指针比较行为。当然,模板不会成为多重评估问题的牺牲品。
  • @Joel:这个答案的重点是函数(模板)可以针对特定的参数类型进行重载(或专门化),而无论参数类型如何,宏都只有一个实现。
  • 这是关于宏的好主意,但并不是问题的真正答案,不是吗?
  • @willj:我同意这是正确的语言功能比宏更安全的重要原因之一,但最好明确说明这一点,而且值得注意的是,即使是模板可以让你做一些傻事。 FWIW,这篇文章让我重新思考了我最近使用模板写的一些东西:)
  • 我在这里遗漏了什么,还是你 max 宏/函数返回它们两个参数中的较低值?
【解决方案4】:

宏不是类型安全的,因为它们不理解类型。

你不能告诉宏只接受整数。预处理器识别宏的用法,并用另一组标记替换一个标记序列(带有其参数的宏)。如果使用得当,这是一个强大的工具,但使用不当很容易。

使用函数,您可以定义函数void f(int, int),如果您尝试使用 f 的返回值或将其传递给字符串,编译器将进行标记。

使用宏 - 没有机会。唯一要做的检查是给定正确数量的参数。然后它会适当地替换标记并传递给编译器。

#define F(A, B)

将允许您致电F(1, 2)、或F("A", 2)F(1, (2, 3, 4))或...

如果宏中的某些内容需要某种类型的安全性,您可能会从编译器中收到错误,也可能不会。但这不取决于预处理器。

将字符串传递给需要数字的宏时,您可能会得到一些非常奇怪的结果,因为您最终可能会使用字符串地址作为数字而不会从编译器中听到。

【讨论】:

  • C++ 模板也是如此。模板也不是类型安全的。
【解决方案5】:

由于宏由预处理器处理,而预处理器不理解类型,它会很乐意接受类型错误的变量。

这通常只是类函数宏的问题,任何类型错误通常都会被编译器捕捉到,即使预处理器没有捕捉到,但这并不能保证。

一个例子

在 Windows API 中,如果您想在编辑控件上显示气球提示,您可以使用 Edit_ShowBalloonTip。 Edit_ShowBalloonTip 定义为采用两个参数:编辑控件的句柄和指向 EDITBALLOONTIP 结构的指针。但是,Edit_ShowBalloonTip(hwnd, peditballoontip); 实际上是一个计算结果为

的宏
SendMessage(hwnd, EM_SHOWBALLOONTIP, 0, (LPARAM)(peditballoontip));

由于配置控件通常是通过向它们发送消息来完成的,Edit_ShowBalloonTip 在其实现中必须进行类型转换,但由于它是宏而不是内联函数,因此它不能在其 peditballoontip 参数中进行任何类型检查.

题外话

有趣的是,有时 C++ 内联函数有点类型安全。考虑标准的 C MAX 宏

#define MAX(a, b) ((a) > (b) ? (a) : (b))

及其 C++ 内联版本

template<typename T>
inline T max(T a, T b) { return a > b ? a : b; }

MAX(1, 2u) 将按预期工作,但 max(1, 2u) 不会。 (由于 1 和 2u 是不同的类型,max 不能在这两个上实例化。)

这并不是真正的支持在大多数情况下使用宏的论据(它们仍然是邪恶的),但它是 C 和 C++ 类型安全的一个有趣结果。

【讨论】:

  • Windows API 示例是一个很好的示例,它看起来像是一个无需强制转换即可接受参数但实际上允许您传递几乎所有垃圾的函数。
  • "...它会很乐意接受类型错误的变量。" 我觉得这个说法没有意义,因为宏参数是无类型的,所以有没有错误或正确的类型。
  • @alk:这不是重点吗? 肯定有错误的类型,就像任何无类型语言都可以有一个函数以意想不到的格式给出数据一样。没有类型,没有类型安全。这就是这里问题的答案。
  • 很容易定义一个可以比较不同类型的最大模板,如果你想要的话。但我实际上会说比较无符号和有符号是错误的,所以你得到一个错误是一件好事。没有太多的类型安全性
  • @jk。 - max(long, int) 或 max(double, int) 如果您愿意。确定用于比较不同类型的最大模板的返回类型并不容易(除非我遗漏了一些东西)。 drdobbs.com/generic-min-and-max-redivivus/184403774 对这个话题很感兴趣。
【解决方案6】:

在某些情况下,宏的类型安全性甚至低于函数。例如

void printlog(int iter, double obj)
{
    printf("%.3f at iteration %d\n", obj, iteration);
}

用颠倒的参数调用它会导致截断和错误的结果,但没有危险。相比之下,

#define PRINTLOG(iter, obj) printf("%.3f at iteration %d\n", obj, iter)

导致未定义的行为。公平地说,GCC 会警告后者,但不会警告前者,但那是因为它知道printf——对于其他可变参数函数,结果可能是灾难性的。

【讨论】:

    【解决方案7】:

    宏不是类型安全的,因为它们从来就不是类型安全的。

    编译器会在宏展开后进行类型检查。

    宏和扩展是作为 C 源代码的(“懒惰”)作者(在作者/读者的意义上)的帮助者。就是这样。

    【讨论】:

    • C++ 模板也是如此。编译器仅在展开(实例化)模板之后 进行类型检查。因此,宏的类型安全性不亚于 C++ 模板。两者都是间接类型安全的,因为编译器最终检查最终生成的代码中的类型。两者都不是直接类型安全的,即 C 宏和 C++ 模板 定义 都不是类型检查的。
    猜你喜欢
    • 1970-01-01
    • 2011-07-01
    • 2011-07-01
    • 2010-09-20
    • 2014-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多