C ++代码的宏危害

定义

C ++语言为不使用宏提供了巨大的可能性。 因此,让我们尝试尽可能少地使用宏!

立即提出保留意见,我不是一个狂热者,也不要出于理想的原因而敦促放弃宏。 例如,当涉及到手动生成相同类型的代码时,我可以认识到宏的好处并接受它们。 例如,我冷静地涉及使用MFC编写的旧程序中的宏。 像这样的战斗是没有意义的:

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP() 

有这样的宏,还可以。 它们的创建实际上是为了简化编程。

我说的是其他宏,它们试图通过这些宏来避免执行完整功能或试图减小功能的大小。 考虑避免这种宏的几种动机。

注意事项 本文是作为Simplify C ++博客的客座帖子撰写的。 我决定在此处发布该文章的俄语版本。 实际上,我写这篇文章是为了避免引起读者的关注,为什么这篇文章没有被标记为“翻译” :)。 实际上,这里是一个英文的来宾帖子:“ C ++代码中的Macro Evil ”。

第一:宏代码吸引错误


我不知道如何从哲学的角度解释这种现象的原因,但事实是这样。 此外,在进行代码审查时,通常很难发现与宏相关的错误。

我在文章中多次描述了这种情况。 例如, 以下宏替换isspace函数:

 #define isspace(c) ((c)==' ' || (c) == '\t') 

使用isspace的程序员认为他正在使用一个真正的函数,该函数不仅将空格和制表符视为空白,而且还将LF,CR等视为。 结果是条件之一始终为真,并且代码无法按预期工作。 这里描述 Midnight Commander的错误。

还是您喜欢这种编写函数std :: printf的简写?

 #define sprintf std::printf 

我认为读者猜测这是一个非常不成功的宏。 顺便说一下,它是在StarEngine项目中找到的。 在此处阅读有关此内容的更多信息。

有人可能会说,程序员应该为这些错误负责,而不是宏。 就是这样 自然,程序员总是要为错误负责:)。

宏会导致错误,这一点很重要。 事实证明,必须以更高的精度使用宏,或者根本不使用宏。

我可以提供长时间与宏使用相关的缺陷示例,这篇不错的笔记将变成一份重量级的多页文档。 当然,我不会这样做,但是我将展示其他一些令人信服的案例。

ATL库提供 A2W,T2W等宏来转换字符串。 但是,很少有人知道在循环内部使用这些宏非常危险。 在宏内部,调用了alloca函数,该函数将在循环的每次迭代中一次又一次地在堆栈上分配内存。 程序可能会假装正常工作。 一旦程序开始处理长行或循环中的迭代次数增加,堆栈就可以在最意外的时刻开始并结束。 您可以在此迷你书中了解更多有关此内容的信息 (请参见“请勿在循环内调用alloca()函数”一章)。

诸如A2W之类的宏隐藏着邪恶。 它们看起来像功能,但实际上具有很难注意到的副作用。

我无法克服使用宏减少代码的类似尝试:

 void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... } 

仅宏的第一行引用if语句 。 不管条件如何,其余行将被执行。 我们可以说这个错误来自C语言世界,因为它是我在GCC编译器中使用V640诊断程序发现的。 GCC代码主要是用C编写的,因此用这种语言编写宏非常困难。 但是,您必须承认事实并非如此。 在这里很有可能实现真正的功能。

第二:代码读取变得更加复杂


如果您遇到一个包含由其他宏组成的宏的项目,那么您了解理解这样的项目到底是什么。 如果您还没有遇到过,那就说一句话,这是可悲的。 作为难以阅读的代码示例,我可以引用前面提到的GCC编译器。

根据传说,由于这些宏,GCC代码的复杂性,Apple已投资开发LLVM项目以替代GCC。 我读过的地方不记得,所以不会有任何证据。

第三:编写宏非常困难


编写错误的宏很容易。 我在任何地方遇到他们都会产生相应的后果。 但是编写一个良好且可靠的宏通常比编写类似的函数困难。

编写良好的宏非常困难,因为与函数不同,它不能被视为独立的实体。 需要立即在所有可能使用的选项的上下文中考虑该宏,否则很容易引发以下形式的问题:

 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]); 

当然,对于这种情况,长期以来就已经找到了解决方法,并且可以安全地实现宏:

 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; }) 

唯一的问题是,在C ++中我们需要所有这些吗? 不,在C ++中,存在用于构建有效代码的模板和其他方法。 那么,为什么我继续在C ++程序中遇到类似的宏?

第四:调试很复杂


有一种观点认为调试是针对弱者的:)。 当然,这值得讨论,但是从实际的角度来看,调试是有用的,并且有助于发现错误。 宏会使此过程复杂化,并且肯定会减慢错误搜索的速度。

第五:静态分析仪的误报


由于宏的设备特性,许多宏会从静态代码分析器生成多个误报。 我可以肯定地说,检查C和C ++代码时,大多数误报与宏相关。

宏的麻烦在于,分析器根本无法将正确的棘手代码与错误代码区分开。 有关检查Chromium的文章介绍了这些宏之一。

怎么办


除非绝对必要,否则不要在C ++程序中使用宏!

C ++提供了丰富的工具,例如模板函数,自动类型推断(auto,decltype),constexpr函数。

几乎总是可以代替宏来编写普通函数。 通常由于普通的懒惰而没有这样做。 这种懒惰是有害的,我们必须与之抗争。 花一点额外的时间来编写一个成熟的功能将使您有兴趣地获得回报。 该代码将更易于阅读和维护。 射击自己的腿的可能性将降低,并且编译器和静态分析器将产生更少的误报。

有人可能会说带有功能的代码效率较低。 这也是“借口”。

现在,即使您没有编写inline关键字 ,编译器也可以完美地内联代码。

如果我们正在谈论在编译阶段计算表达式,那么这里不需要宏,甚至是有害的。 出于相同的目的,使用constexpr会更好,更安全。

我将举例说明。 这是我从FreeBSD内核代码中借来的经典宏错误。

 #define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... } 

chan参数在宏中使用,而不用括号括起来。 结果,表达式ICB2400_VPOPT_WRITE_SIZE不会将表达式(chan-1)相乘,而只能乘以一个。

如果编写普通函数而不是宏,则不会出现该错误。

 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

现代C和C ++编译器很可能将独立执行函数的内联 ,并且代码将与宏一样高效。

同时,代码变得更具可读性,并且没有错误。

如果知道输入值始终是常数,则可以添加constexpr并确保所有计算都将在编译阶段进行。 想象一下,它是C ++,而chan始终是常数。 然后像这样声明函数ICB2400_VPINFO_PORT_OFF是很有用的:

 constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

赢利!

我希望我能说服你。 祝您好运,代码中的宏更少!

Source: https://habr.com/ru/post/zh-CN444612/


All Articles