C++宏何时有益?

182
C预处理器在C++社区中备受恐惧和回避。内联函数、常量和模板通常是#define的更安全、更优越的替代方案。
下面是宏:
#define SUCCEEDED(hr) ((HRESULT)(hr) >= 0)  

并不比类型安全更优越:

inline bool succeeded(int hr) { return hr >= 0; }

但是宏确实有其用处,请列出您发现需要预处理器无法完成的宏使用情况。

请将每个用例放在单独的答案中,以便进行投票。如果您知道如何在没有预处理器的情况下实现其中的某个答案,请在该答案的评论中指出。


我曾经拿到一个充满宏定义的 C++ 应用程序,构建需要 45 分钟的时间。我将这些宏定义替换为内联函数后,构建时间缩短至不到 15 分钟。 - endian
静态断言 - Özgür
本主题讨论的是宏有益的情况,而不是它们不佳的情况。 - underscore_d
@Özgür 你想说什么? - John
38个回答

17

C++的单元测试框架(例如UnitTest++)基本上都围绕着预处理宏展开。少量的单元测试代码会扩展成一系列类的层级结构,如果手动输入将不太好玩。如果没有像UnitTest++那样的工具和其预处理魔法,我不知道您如何有效地编写C++单元测试。


单元测试完全可以在没有框架的情况下编写。最终,这只取决于您想要什么样的输出。如果您不关心,一个简单的退出值指示成功或失败应该是完全可以的。 - Clearer

16

使用普通函数调用无法对函数调用参数进行短路计算。例如:

#define andm(a, b) (a) && (b)

bool andf(bool a, bool b) { return a && b; }

andm(x, y) // short circuits the operator so if x is false, y would not be evaluated
andf(x, y) // y will always be evaluated

4
可能更常见的一点是:函数会精确计算其参数一次。宏可以多次或少次地计算参数。 - Steve Jessop
@[Greg Rogers] 所有的宏预处理器只是替换文本。一旦你理解了这一点,就不应该再有什么神秘感了。 - 1800 INFORMATION
你可以通过将其模板化而不是在调用函数之前强制求值为bool来获得等效的行为。不过,如果没有自己尝试过,我就不会意识到你所说的是真的。有趣。 - Greg Rogers
你怎样使用模板来实现这一点? - 1800 INFORMATION
9
在生产代码中,我真的不想看到将短路操作隐藏在函数式宏背后的做法。 - MikeMB

14
害怕C预处理器就像害怕白炽灯泡一样,因为我们有荧光灯泡。是的,前者可能会导致{电力 | 程序员时间}效率低下。是的,你可能会(字面上)被它们烧伤。但如果你正确地处理它们,它们可以完成工作。
当您编写嵌入式系统时,C除了汇编语言之外似乎是唯一的选择。在使用C ++在台式机上编程,然后切换到更小的嵌入式目标后,您学会停止担心许多裸C特性(包括宏)的“不文雅之处”,并尝试找出最佳和安全的用法。
亚历山大·斯特潘诺夫

当我们在C ++中编程时,我们不应该为它的C遗产感到羞耻,而是充分利用它。 C ++的唯一问题,甚至C的唯一问题,是当它们本身与其自身的逻辑不一致时。


我认为这是错误的态度。仅仅因为你可以学会“正确处理它”并不意味着它值得任何人花费时间和精力。 - Neil G

10

第三个链接已经失效,供您参考。 - Robin Hartland
请查看vc12中的stdio.hsal.h文件以更好地理解。 - Elshan

9
我们在信息丰富的异常抛出、捕获和日志记录中使用__FILE____LINE__宏,以及我们QA基础设施中的自动化日志文件扫描工具。
例如,一个抛出宏OUR_OWN_THROW可能会与该异常的异常类型和构造函数参数一起使用,包括文本描述。如下所示:
OUR_OWN_THROW(InvalidOperationException, (L"Uninitialized foo!"));

这个宏会抛出InvalidOperationException异常,其中包含构造函数参数的描述信息,但它还会写入一条消息到日志文件中,该消息由抛出异常的文件名和行号及其文本描述组成。抛出的异常将获得一个ID,并在日志中记录。如果代码的其他地方捕获了异常,则会标记该异常,并且日志文件将指示特定的异常已被处理,因此不太可能是后续记录的任何崩溃的原因。我们的自动化QA基础设施可以轻松捕获未处理的异常。


8

代码重复。

请查看boost预处理器库,它是一种元元编程。在主题->动机中,您可以找到一个很好的例子。


在几乎所有情况下,如果不是全部情况,通过函数调用可以避免代码重复。 - einpoklum
@einpoklum:我不同意。请看链接。 - Ruggero Turra

7

一个常见的用途是检测编译环境,对于跨平台开发,您可以为Linux编写一组代码,而对于Windows,则可以编写另一组代码,当不存在适用于您目的的跨平台库时。

因此,在一个粗略的例子中,跨平台互斥锁可以具有以下特点:

void lock()
{
    #ifdef WIN32
    EnterCriticalSection(...)
    #endif
    #ifdef POSIX
    pthread_mutex_lock(...)
    #endif
}

对于函数而言,当您想要明确忽略类型安全时,它们非常有用。例如上下文中进行ASSERT的许多示例。当然,像许多C/C++特性一样,您可能会自找麻烦,但语言会为您提供工具,并让您决定如何处理。


自从提问者问道:可以通过在每个平台上通过不同的包含路径包含不同的头文件来实现这一点,而不必使用宏。尽管如此,我倾向于认为宏通常更方便。 - Steve Jessop
我赞同。如果你开始使用宏来实现这个目的,代码很快就会变得难以阅读。 - Nemanja Trifunovic

7
我偶尔会使用宏来在一个地方定义信息,然后在代码的不同部分以不同的方式使用它。这只是稍微有些“邪恶” :)

例如,在“field_list.h”中:
/*
 * List of fields, names and values.
 */
FIELD(EXAMPLE1, "first example", 10)
FIELD(EXAMPLE2, "second example", 96)
FIELD(ANOTHER, "more stuff", 32)
...
#undef FIELD

对于公共枚举,可以定义为仅使用名称:

#define FIELD(name, desc, value) FIELD_ ## name,

typedef field_ {

#include "field_list.h"

    FIELD_MAX

} field_en;

在私有的初始化函数中,所有字段都可以用来填充数据表:

#define FIELD(name, desc, value) \
    table[FIELD_ ## name].desc = desc; \
    table[FIELD_ ## name].value = value;

#include "field_list.h"

2
注意:即使没有单独的包含文件,也可以实现类似的技术。请参见:https://dev59.com/-XVC5IYBdhLWcg3w51ryhttps://dev59.com/l3VC5IYBdhLWcg3w-mZO - Suma

6

类似于

void debugAssert(bool val, const char* file, int lineNumber);
#define assert(x) debugAssert(x,__FILE__,__LINE__);

这样你就可以举个例子

assert(n == true);

如果n为false,则在日志中打印出问题的源文件名和行号。

如果您使用普通的函数调用,例如

void assert(bool val);

与其使用宏,你只能在日志中得到断言函数所在的行号,这对于问题的定位不够有帮助。


当标准库的实现已经通过 <cassert> 提供了 assert() 宏,可以输出文件/行号/函数信息时,为什么还要重新发明轮子呢?(至少在我看到的所有实现中都是这样) - underscore_d

4
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])

与当前线程中讨论的“首选”模板解决方案不同,您可以将其用作常量表达式:
char src[23];
int dest[ARRAY_SIZE(src)];

2
这可以通过模板以更安全的方式完成(如果传递指针而不是数组,则无法编译)。 - Motti
1
现在我们在C++11中有了constexpr,安全的(非宏)版本也可以在常量表达式中使用。template<typename T, std::size_t size> constexpr std::size_t array_size(T const (&)[size]) { return size; } - David Stone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接