C ++语言为不使用宏提供了巨大的可能性。 因此,让我们尝试尽可能少地使用宏!
立即提出保留意见,我不是一个狂热者,也不要出于理想的原因而敦促放弃宏。 例如,当涉及到手动生成相同类型的代码时,我可以认识到宏的好处并接受它们。 例如,我冷静地涉及使用MFC编写的旧程序中的宏。 像这样的战斗是没有意义的:
BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT )
有这样的宏,还可以。 它们的创建实际上是为了简化编程。
我说的是其他宏,它们试图通过这些宏来避免执行完整功能或试图减小功能的大小。 考虑避免这种宏的几种动机。
注意事项 本文是作为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))
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; }
赢利!
我希望我能说服你。 祝您好运,代码中的宏更少!