【问题标题】:Do macros make the code more readable? [closed]宏是否使代码更具可读性? [关闭]
【发布时间】:2012-07-04 16:51:08
【问题描述】:

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

例如:

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

【问题讨论】:

  • 如果你传入__LINE__,你可以对函数做同样的事情。
  • 我会说 C 中的宏没有任何问题。另一方面,在 C++ 中,它们就像牌匾:最好避免使用!仅他们不遵循作用域这一事实就应该表明宏是多么邪恶!
  • 我通常两者都做:像exitOnFailure这样的函数和只传递参数的宏EXIT_ON_FAILURE`加上__FILE____LINE____func__
  • @MFH 他们是一块牌匾?使用these?我想你的意思是瘟疫。
  • 看来您正在重新发明 assert() 宏。

标签: c++ c code-readability


【解决方案1】:

宏是 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-like 宏绑定到本地调试方案,以及那些 几乎总是使用宏来实现以获取__FILE____LINE__ 和条件文本。

但即便如此,典型的assert 是这样实现的:

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

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

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

最后一件事。如果你想使用宏,因为你认为 代码会更快,请注意这是魔鬼在说话。在 过去,可能存在转换小 宏中的函数提供了显着的性能改进。这些 不过几天:

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

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

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

也许吧。

【讨论】:

  • +1 表示“如果您想使用宏,因为您认为代码会更快,请注意这是魔鬼在说。”当你过早优化时,you optimize with Hitler!
  • 很好的答案!尤其是良好的宏使用示例(调试断言)。
【解决方案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 &gt;= 20) foo(),它会扩展到 if (5 + 5 * 2 &gt;= 20) foo()。因此,您需要记住在定义宏时始终在变量周围使用 ()。

宏也使程序更难调试。

【讨论】:

    【解决方案3】:

    有时您确实需要宏,但您应该尽量减少它们的数量。在您的示例中,已经有一个名为“assert”的宏,您可以使用它来代替创建新宏。

    C++ 有许多特性可以让您在没有宏的情况下执行需要 C 中的宏的操作,因此宏在 C++ 代码中的使用应该比在 C 代码中更少。

    【讨论】:

    • 有时您不想使用断言,因为它可能会吓到您的用户。在这些情况下,信息输出可能会更好。
    • @WhiteZebra 他们应该害怕什么? assert 背后的想法是在调试版本中使用它,以便在软件发布之前发现可预防的问题。由于这个原因,断言宏在发布模式下被定义为无操作(参见en.cppreference.com/w/cpp/error/assert)...
    猜你喜欢
    • 1970-01-01
    • 2012-07-02
    • 1970-01-01
    • 2010-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-17
    相关资源
    最近更新 更多