2

我就宏及其可读性进行了辩论。我认为在某些情况下使用宏可以使代码更短、更易于理解并且阅读起来不那么累。

例如:

#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++ 中,还是只是不好(但可以接受)的风格?

4

3 回答 3

3

宏是 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'd 常量一样,但它也会以您期望的方式处理类型转换,并将显示在调试器的符号表中。

如果您需要类似内联函数的东西,请使用内联函数。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. 现代计算机是如此之快,以至于你几乎肯定不会注意到调用一个微不足道的函数的开销。

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

也许。

于 2012-07-05T20:09:48.330 回答
0

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

在您的示例中,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()。因此,在定义宏时,您需要记住始终在变量周围使用 ()。

宏也使程序更难调试。

于 2012-07-05T14:13:49.773 回答
0

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

C++ 有许多特性允许你在没有宏的情况下做一些需要 C 中的宏的事情,所以宏在 C++ 代码中应该比在 C 代码中更不常见。

于 2012-07-05T14:23:39.430 回答