宏是否使代码更具可读性

Do macros make the code more readable?

本文关键字:可读性 代码 是否      更新时间:2023-10-16

我有一个关于宏及其可读性的辩论。我认为在某些情况下,使用宏可以使代码更短、更易于理解且阅读起来更不累。

例如:

#include <iostream>
#define EXIT_ON_FAILURE(s) if(s != 0) {std::cout << "Exited on line " << __LINE__ << std::endl; exit(1);}
inline void exitOnFailure(int s, int lineNum) {
    if (s != 0) {
        std::cout << "Exited on line " << lineNum << std::endl; 
        exit(1);
    }
}
int foo() {
    return 1;
}
int bar(int a, int b, int c) {
    return 0;
}
int main() {
    // first option
    if (foo() != 0) {
        std::cout << "Exited on line " << __LINE__ << std::endl;
        exit(1);    
    }
    if (bar(1, 2, 3) != 0) {
        std::cout << "Exited on line " << __LINE__ << std::endl;
        exit(1);    
    }
    // second option
    EXIT_ON_FAILURE(foo());
    EXIT_ON_FAILURE(bar(1, 2, 3));
    // third option
    exitOnFailure(foo(), __LINE__);
    exitOnFailure(bar(1, 2, 3), __LINE__);
    return 0;
}

我更喜欢这里的第二个选项,因为它简短紧凑,并且大写锁定文本比驼峰大小写更清晰、更易于阅读。

这种方法有什么问题,尤其是在C++中,还是只是糟糕(但可以接受)的风格?

是 C/C++ 的一个非常强大的功能,就像所有 C 功能一样默认指向您的脚。 考虑以下使用您的宏:

if (doSomething())
    EXIT_ON_FAILURE(s)   /* <-- MISSING SEMICOLON! OH NOES!!! */
else
    doSomethingElse();

else属于语句中的if还是if通过扩展EXIT_ON_FAILURE创建? 无论哪种方式,行为缺少一个分号是完全出乎意料的。 如果EXIT_ON_FAILURE()是一个函数,你会得到一个编译器错误。 在这种情况下,你会得到编译但执行错误操作的代码。

这就是宏的问题。 它们看起来像函数或变量,但事实并非如此。 写得不好的宏是礼物不断给予。 宏的每次使用都是一个潜在的微妙错误,并且对宏的每次更改都有可能在代码中引入逻辑错误你没有碰。

通常,除非绝对必要,否则应避免使用宏。

如果需要定义常量,请使用 const 变量或 enum 。一个好的编译器(您可以免费获得)会将它们变成生成的可执行文件中的文字,就像一个 #define 常量一样但它也会按照您期望的方式处理类型转换,并且会显示在调试器的符号表中。

如果您需要类似内联函数

的东西,请使用内联函数。C++ 和 C99 都提供了它们和最体面的编译器(包括免费)已经作为扩展做了很长时间。

函数强制计算其参数,与宏不同,因此

inline int DOUBLE(int a) {return a+a;}

只会评估a一次

#define DOUBLE(a) (a + a)

将评估a两次。 这意味着

x = DOUBLE(someVeryLongFunction());

如果 DOUBLE 是宏,则花费的时间是函数的两倍。

另外,我(故意)忘记用括号括起来的宏参数,所以这:

DOUBLE(a << b)

将给出一个完全令人惊讶的结果。 你需要记住将 DOUBLE 宏写为

#define DOUBLE(a) ((a) + (a))

换句话说,您需要完美地编写宏以最小化搬起石头砸自己的脚的机会。 如果你犯了一个错误,您将为此付出多年的代价。

综上所述,是的,在某些情况下,宏会使代码更具可读性。 他们很少而且相距甚远,但他们存在。 其中之一是您的代码重新发明的assert宏。复杂系统使用自己的自定义是很常见的 assert类似宏的宏绑定到本地调试方案中,以及那些几乎总是用宏实现,以获得__FILE____LINE__和条件的文本。

但即便如此,这也是典型assert的实现方式:

#ifdef NDEBUG
#   define assert(cond)
#else
#   define assert(cond) __assert(cond, __FILE__, __LINE__, #cond)
#endif

换句话说,类似函数的宏扩展为函数调用。这样,当您调用assert时,扩展仍然非常接近到它的样子和参数扩展的方式发生你会期望它。

还有一些其他用途。 基本上,任何时候你需要将信息从构建过程本身传递给程序,它将可能需要通过宏观系统。 即便如此,你应尽量减少接触宏的代码量以及如何宏做了很多。

最后一件事。 如果您因为认为而想使用宏代码会更快,请注意这是魔鬼在说话。 在过去,可能有一些情况,转换小宏中的函数带来了明显的性能改进。 这些虽然天:

  1. 大多数编译器都支持内联函数。 有些人甚至这样做自动到静态函数。

  2. 现代计算机的速度如此之快,以至于您几乎肯定不会注意到调用甚至微不足道的函数的开销。

仅当你的编译器不执行内联函数并且你不能只是用更好的替换它你已经证明了函数调用开销是一个瓶颈,您是否可以证明编写一些宏是合理的。

或。

当然,宏可以简化函数,使其更易于阅读。但您应该考虑改用内联函数。

在您的示例中,EXIT_ON_FAILURE可以是内联函数。宏不仅使编译器错误不准确(它可能会导致一些错误显示在错误的地方),在使用宏时需要注意一些事项,特别是变量,请考虑以下示例:

#define MY_MACRO(s) if (s * 2 >= 20) foo()
// later on your code:
MY_MACRO(5 + 5);

虽然人们可以期望 foo() 对我来说是调用的,但它不会,因为它不会扩展到if (10 * 2 >= 20) foo(),它会扩展到if (5 + 5 * 2 >= 20) foo()。因此,您需要记住在定义宏时始终在变量周围使用 ()。

宏还使程序更难调试。

当然,

有时宏是您需要的,但您应该尽量减少它们的数量。 在您的示例中,已经有一个名为"assert"的宏,您可以使用它而不是创建一个新宏。

C++ 具有许多功能,允许您在没有宏的情况下执行操作,而宏在 C 中需要宏,因此宏在C++代码中应该比在 C 代码中更不常见。