我一直在使用一个主要由不再在公司工作的程序员编写的大型代码库。其中一个程序员显然在他的心中有一个特殊的位置,用于非常长的宏。我可以看到使用宏的唯一好处是能够编写不需要在所有参数中传递的函数(建议在我阅读的最佳实践指南中使用)。除此之外,我认为内联函数没有任何好处。
有些宏是如此复杂,我很难想象有人甚至写它们。我尝试用这种精神创造一个,这是一场噩梦。调试非常困难,因为它在调试器中将N +行代码转换为1(例如,在这个大块代码中的某处有一个段错误。祝你好运!)。我必须实际拉出宏并运行它非宏观调试它。我能看到这个人编写这些内容的唯一方法就是在他调试之后用一个函数编写的代码自动生成它们(或者通过比我更聪明并且第一次完美地编写它,我猜这总是可能的)
我错过了什么吗?我疯了吗?有没有我不知道的调试技巧?请填写我。我真的很想听听观众中的宏观爱好者。 :)
答案 0 :(得分:8)
对我来说,宏的最佳用途是压缩代码并减少错误。缺点显然是在调试中,因此必须小心使用它们。
我倾向于认为如果生成的代码不是一个数量级更小并且更不容易出错(意味着宏会处理一些簿记细节),那么它就不值得了。
在C ++中,像这样的许多用法可以用模板替换,但不是全部。有用的宏的一个简单示例是在MFC的事件处理程序宏中 - 没有它们,创建事件表将更难以正确完成,而您必须编写(和读取)的代码将更加复杂。
答案 1 :(得分:4)
如果宏非常长,它们可能会使代码变短但效率很高。实际上,他可能已经使用宏来显式内联代码或从运行时代码路径中删除决策点。
可能很重要的是要理解,在过去,许多编译器并没有完成这样的优化,而今天我们认为理所当然的一些事情,如快速函数调用,则无效。
答案 2 :(得分:3)
对于前。看到这个短宏:
#define max(a, b) ((a)>(b)?(a):(b))
然后尝试这个电话:
max(i++, j++)
更多。说你有
#define PLANETS 8
#define SOCCER_MIDDLE_RIGHT 8
如果抛出错误,它将引用'8',但不是它的任何一个有意义的表示。
答案 3 :(得分:3)
我只知道做你所描述的两个理由。
首先强制函数内联。这几乎没有意义,因为内联关键字通常做同样的事情,而函数内联通常是过早的微优化。
其次是用C或C ++模拟嵌套函数。这与你的“编写函数不需要在所有参数中传递”有关,但实际上可能比这更强大。 Walter Bright gives嵌套函数可能有用的示例。
使用宏还有其他原因,例如使用预处理器特定的功能(如在自动生成的错误消息中包含__FILE__
和__LINE__
)或以函数和模板的方式减少样板代码' t(Boost.Preprocessor库在这里擅长;有关示例,请参阅Boost.ScopeExit或this sample enum code),但这些原因似乎并不适用于您所描述的内容。
答案 4 :(得分:2)
非常长的宏会有性能上的缺陷,比如增加编译的二进制文件大小,并且肯定有其他原因不使用它们。
对于最有问题的宏,我会考虑通过预处理器运行代码,并用函数调用(如果可能的话内联)或直接LOC替换宏输出。如果存在与其他体系结构/操作系统兼容的宏,则可能会遇到困难。
答案 5 :(得分:1)
部分好处是没有最终维护成本的代码复制 - 也就是说,不是在其他地方复制代码,而是从中创建宏,只需编辑一次......
当然,你也可以制作一个方法来调用,但这是更多的工作......我自己反对宏观用法,只是试图提出一个潜在的理由。
答案 6 :(得分:1)
在C中编写宏有很多好的理由。
一些最重要的是使用x-macro创建配置表,用于创建类似宏的函数,可以接受多个参数类型作为输入,并将表从人类可读/可配置/可理解的值转换为计算机使用的值。
我无法真正看到人们编写非常长的宏的原因,除了历史自动函数内联。
我会说在调试复杂宏时,(当编写 X 宏等时)我倾向于预处理源文件并将预处理文件替换为原始文件。 / p>
这允许您查看生成的C代码,并为您提供在调试器中使用的实线。
答案 7 :(得分:0)
我根本不使用宏。内联函数服务于宏可以执行的每个有用的目的。宏允许你做一些非常奇怪和违反直觉的事情,比如拆分标识符(那么有人如何搜索标识符?)。
答案 8 :(得分:0)
我还参与了一项产品,其中一位传统程序员(感谢早已离去)也与Macros有着特别的恋情。他的“定制”脚本语言是邋。的高度。他在C语言中编写了C ++类,这意味着所有类函数和变量都是公共的。无论如何,他几乎用宏观和可变函数写出了所有东西(世界上另一个可怕的怪物)。所以不是写一个合适的模板类,而是使用宏代替!他还使用宏来创建工厂类,而不是普通的代码......他的代码几乎是不可思议的。
从我所看到的,宏可以在它们很小时使用并且以声明方式使用,并且不包含诸如循环之类的移动部分以及其他程序流表达式。如果宏是一行或最多两行,并且它声明了某事物的实例,那就没关系。在运行时不会破坏的东西。宏也不应该包含类定义或函数定义。如果宏包含需要使用调试器进入的代码,则应删除宏并替换为其他内容。
它们对于包装自定义跟踪/调试功能也很有用。例如,您希望在调试版本中进行自定义跟踪,而不是发布版本。
无论如何,当你在这样的遗留代码中工作时,一定要一次删除一点宏的混乱。如果你坚持下去,有足够的时间,你最终将把它们全部删除,让自己的生活更轻松。我过去做过这个,特别是凌乱的宏。我所做的是打开编译器开关让预处理器生成一个输出文件。然后我突袭该文件,复制代码,重新缩进,并用生成的代码替换宏。谢天谢地,感谢您的编译器功能。
答案 9 :(得分:0)
我使用过的一些遗留代码在方法的位置上使用了非常广泛的宏。原因是计算机/ OS /运行时具有极小的堆栈,因此堆栈溢出是一个常见问题。使用宏而不是方法意味着堆栈中的方法更少。
幸运的是,大部分代码已经过时,所以现在(大部分)已经消失了。
答案 10 :(得分:0)
C89没有内联功能。如果使用禁用扩展的编译器(由于多种原因这是一件令人满意的事情),那么宏可能是唯一的选择。
虽然C99在1999年问世,但长期以来一直存在阻力;商业编译器供应商并不觉得值得花时间来实现C99。有些人(例如MS)还没有。因此,对于许多公司而言,使用C99符合模式并不是一个可行的实际决定,即使在某些编译器的情况下也是如此。
我使用的C89编译器确实具有内联函数的扩展名,但扩展名有错误(例如,当不应该存在多个定义错误时),这样的事情可能会阻止程序员使用内联函数。
另一件事是宏版本有效地强制该函数实际上将被内联。 C99 inline
关键字只是一个编译器提示,编译器可能仍然决定生成一个像非内联函数链接的函数代码的单个实例。 (如果函数不是很简单并且返回void
),我仍然使用的一个编译器将执行此操作。