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个回答

129
作为调试函数的包装器,自动传递像__FILE____LINE__等内容:
#ifdef ( DEBUG )
#define M_DebugLog( msg )  std::cout << __FILE__ << ":" << __LINE__ << ": " << msg
#else
#define M_DebugLog( msg )
#endif

自从C++20版本开始,魔术类型std::source_location可以代替__LINE____FILE__来实现一个普通函数(模板)的类似功能。

15
实际上,原始代码片段: << FILE ":" << 是正确的,FILE 会生成一个字符串常量,它将由预处理器与 ":" 连接成一个单独的字符串。 - Frank Szczerba
12
这只需要预处理器,因为 __FILE____LINE__ 需要预处理器。在你的代码中使用它们就像是预处理器的感染向量。 - T.E.D.
1
@T.E.D. 为什么“在你的代码中使用它们就像是预处理器的感染向量”?你能为我详细解释一下吗? - John
@John - 十年后问:哇。我记得的一个例子是一个旧的日志记录设施,设计为传递这些值,我想简化/现代化为基于流的方式。我遇到的问题是我必须将流对象也制作成宏,以便它们可以自动填充这些值。如果您尝试使用直接代码,每个日志消息都会获得日志流对象内部的文件和行号。 - T.E.D.

96

方法必须始终是可编译的完整代码;宏可以是代码片段。因此,您可以定义一个foreach宏:

#define foreach(list, index) for(index = 0; index < list.size(); index++)

然后这样使用它:

foreach(cookies, i)
    printf("Cookie: %s", cookies[i]);

自C++11以来,这被基于范围的for循环所取代。


6
如果您正在使用某些非常复杂的迭代器语法,编写一个类似于foreach的宏可以使您的代码更易于阅读和维护。我已经尝试过了,它很有效。+1 - postfuturist
10
大多数评论与宏可能是代码片段而不是完整代码的观点完全无关。但感谢您的吹毛求疵。 - jdmichal
13
这是C语言而不是C++。如果你在使用C++,应该使用迭代器和std::for_each。 - chrish
23
我不同意,Chrish。在lambda出现之前,for_each是一件麻烦的事情,因为每个元素运行的代码不属于调用点的本地范围。foreach(我强烈建议使用BOOST_FOREACH而不是手动编写解决方案)可以让你将代码保持靠近迭代位置,使其更易读。话虽如此,一旦lambda开始使用,for_each可能再次成为最佳选择。 - GManNickG
8
值得注意的是,BOOST_FOREACH 本身是一个宏(但非常精心设计)。 - Tyler McHenry
显示剩余8条评论

61

头文件保护需要使用宏。

还有其他领域需要使用宏吗?没有多少(如果有的话)。

还有哪些情况可以从宏中受益呢?当然可以!

我使用宏的一个场景是处理非常重复的代码。例如,将C++代码封装以便与其他接口(.NET、COM、Python等)一起使用时,我需要捕获不同类型的异常。以下是我的做法:

#define HANDLE_EXCEPTIONS \
catch (::mylib::exception& e) { \
    throw gcnew MyDotNetLib::Exception(e); \
} \
catch (::std::exception& e) { \
    throw gcnew MyDotNetLib::Exception(e, __LINE__, __FILE__); \
} \
catch (...) { \
    throw gcnew MyDotNetLib::UnknownException(__LINE__, __FILE__); \
}

我必须在每个包装函数中放置这些捕获块。为了避免每次输入完整的catch块,我只需键入:

void Foo()
{
    try {
        ::mylib::Foo()
    }
    HANDLE_EXCEPTIONS
}

这也使得维护更加容易。如果我需要添加一个新的异常类型,只需要在一个地方添加即可。

还有其他有用的例子:其中许多包括__FILE____LINE__预处理器宏。

总之,宏在正确使用时非常有用。宏本身并不邪恶——它们的滥用才是邪恶的。


7
现在大多数编译器都支持#pragma once,所以我怀疑守卫是否真的必要。 - 1800 INFORMATION
15
如果你是在为所有编译器编写代码,而不仅仅是大多数编译器编写代码的话,那么这个条件就会起作用。;-) - Steve Jessop
35
所以,你建议使用预处理器扩展而不是便携式的标准预处理器功能,以避免使用预处理器?在我看来,这似乎有些荒谬。 - Logan Capaldo
#pragma once 在许多常见的构建系统上会出现问题。 - Miles Rout
5
有一种不需要宏的解决方案:void handleExceptions(){ try { throw } catch (::mylib::exception& e) {....} catch (::std::exception& e) {...} ... }。在函数方面:void Foo(){ try {::mylib::Foo() } catch (...) {handleExceptions(); } } - MikeMB
1
我不明白这里使用宏的意义。你可以定义一个类似于std::invoke的函数模板,并在其中处理异常。基本上,这是一个高阶函数,调用提供的函数并处理任何错误。 - Xeverous

55

主要包括:

  1. 包含保护
  2. 条件编译
  3. 报告(预定义的宏,如__LINE____FILE__
  4. (很少需要)复制重复的代码模式。
  5. 在竞争对手的代码中。

2
寻求一些关于如何实现第5个数字的帮助。你能指导我找到解决方案吗? - Max
1
@David Thornley,您能否给我展示一个“条件编译”的例子? - John
3
@John • #ifdef _WIN32 /* 做Windows的事情 */ #elif __linux__ /* 做Linux的事情 */ #elif __APPLE__ /* 做Macintosh的事情 */ #else #error 不支持的操作系统 #endif - Eljay

50

在条件编译中,为了克服不同编译器之间的差异问题:

#ifdef WE_ARE_ON_WIN32
#define close(parm1)            _close (parm1)
#define rmdir(parm1)            _rmdir (parm1)
#define mkdir(parm1, parm2)     _mkdir (parm1)
#define access(parm1, parm2)    _access(parm1, parm2)
#define create(parm1, parm2)    _creat (parm1, parm2)
#define unlink(parm1)           _unlink(parm1)
#endif

14
在C++中,可以通过使用内联函数来实现相同的效果:<code>#ifdef ARE_WE_ON_WIN32 <br>inline int close(int i) { return _close(i) ; } <br> #endif</code>。 - paercebal
2
这会移除 #define,但不会移除 #ifdef 和 #endif。无论如何,我同意你的观点。 - Gorpik
21
绝对不要定义小写字母的宏。用于修改函数的宏是我的噩梦(谢谢你,微软)。最好的例子在第一行中。许多库都有“close”函数或方法。当您包含此库的头文件和带有此宏的头文件时,就会出现大问题,您将无法使用库API。 - Marek R
@einpoklum - 我已经很久没有写过这个了。@paercebal的建议确实去掉了#define。当时我们需要用于C和C++,所以就这样... - Andrew Stein
2
#ifdef WE_ARE_ON_WIN32 请 :) - Lightness Races in Orbit
显示剩余2条评论

37

当你想将一个表达式转换为字符串时,最好的例子是 assert#x将变量x的值转换为字符串)。

#define ASSERT_THROW(condition) \
if (!(condition)) \
     throw std::exception(#condition " is false");

8
个人认为应该省略分号。 - Michael Myers
13
我同意,实际上我会把它放在一个 do {} while(false) 里(以防止 else 被劫持),但我想保持简单。 - Motti

34

有时候,字符串常量最好定义为宏,因为你可以对字符串字面量做更多的操作,而不是使用const char *

例如,字符串字面量可以很容易地进行拼接

#define BASE_HKEY "Software\\Microsoft\\Internet Explorer\\"
// Now we can concat with other literals
RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "Settings", &settings);
RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "TypedURLs", &URLs);
如果使用了const char *,则必须使用某种字符串类来在运行时执行连接操作。
const char* BaseHkey = "Software\\Microsoft\\Internet Explorer\\";
RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "Settings").c_str(), &settings);
RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "TypedURLs").c_str(), &URLs);

自 C++20 开始,可以实现一种类似于字符串的类类型,可用作用户定义的字符串字面量运算符的非类型模板参数类型,从而允许进行编译时的连接操作而无需使用宏。


4
在C++11中,我认为这是最重要的部分(除了include guards之外)。宏确实是我们用于编译时字符串处理的最好工具。这是一个特性,我希望我们在C++11++中能够得到。 - David Stone
3
这种情况促使我希望在C#中使用宏。 - Rawling
3
我希望我能够点赞这个评论(+42)。这是一个非常重要但常常被忽视的字符串常量方面。 - Daniel Kamil Kozar

24

当您想要更改程序流程(returnbreakcontinue)时,函数中的代码与实际内联在函数中的代码行为不同。

#define ASSERT_RETURN(condition, ret_val) \
if (!(condition)) { \
    assert(false && #condition); \
    return ret_val; }

// should really be in a do { } while(false) but that's another discussion.

抛出异常对我来说似乎是更好的选择。 - einpoklum
在编写Python C(++)扩展时,通过设置异常字符串,然后返回“-1”或“NULL”来传播异常。因此,在那里,宏可以大大减少样板代码。 - black_puppydog

20

显而易见的包含保护

#ifndef MYHEADER_H
#define MYHEADER_H

...

#endif

17

假设我们忽略像头文件保护这样的显而易见的事情。

有时候,您需要生成需要由预编译器复制/粘贴的代码:

#define RAISE_ERROR_STL(p_strMessage)                                          \
do                                                                             \
{                                                                              \
   try                                                                         \
   {                                                                           \
      std::tstringstream strBuffer ;                                           \
      strBuffer << p_strMessage ;                                              \
      strMessage = strBuffer.str() ;                                           \
      raiseSomeAlert(__FILE__, __FUNCSIG__, __LINE__, strBuffer.str().c_str()) \
   }                                                                           \
   catch(...){}                                                                \
   {                                                                           \
   }                                                                           \
}                                                                              \
while(false)

这使您能够编写以下代码:

RAISE_ERROR_STL("Hello... The following values " << i << " and " << j << " are wrong") ;

并且可以生成像这样的消息:

Error Raised:
====================================
File : MyFile.cpp, line 225
Function : MyFunction(int, double)
Message : "Hello... The following values 23 and 12 are wrong"

请注意,将模板与宏混合使用可以带来更好的结果(即在变量名称旁边自动生成值)。

其他时候,您需要获取某些代码的 __FILE__ 和/或 __LINE__ ,例如生成调试信息。下面是Visual C++的经典用法:

#define WRNG_PRIVATE_STR2(z) #z
#define WRNG_PRIVATE_STR1(x) WRNG_PRIVATE_STR2(x)
#define WRNG __FILE__ "("WRNG_PRIVATE_STR1(__LINE__)") : ------------ : "

与以下代码一样:

#pragma message(WRNG "Hello World")

它会生成像这样的消息:

C:\my_project\my_cpp_file.cpp (225) : ------------ Hello World

有时候,你需要使用 # 和 ## 连接运算符生成代码,比如为一个属性生成 getter 和 setter(尽管这只适用于非常有限的情况)。

但有时候,你会生成一些在函数中使用会无法编译的代码,例如:

#define MY_TRY      try{
#define MY_CATCH    } catch(...) {
#define MY_END_TRY  }
可以用作什么?
MY_TRY
   doSomethingDangerous() ;
MY_CATCH
   tryToRecoverEvenWithoutMeaningfullInfo() ;
   damnThoseMacros() ;
MY_END_TRY

(不过,我只看到这种代码被正确使用一次)

最后,但同样重要的是,著名的boost::foreach !!!

#include <string>
#include <iostream>
#include <boost/foreach.hpp>

int main()
{
    std::string hello( "Hello, world!" );

    BOOST_FOREACH( char ch, hello )
    {
        std::cout << ch;
    }

    return 0;
}
(Note: code copy/pasted from the boost homepage)
(注:代码从boost主页复制/粘贴)
在我看来,这比std::for_each要好得多。
因此,宏始终很有用,因为它们不受正常编译器规则的限制。但是我发现,大部分情况下我看到的宏实际上是C代码的残留物,从未转换为适当的C ++。

3
仅在编译器无法完成的情况下使用CPP。例如,RAISE_ERROR_STL应仅使用CPP来确定文件、行和函数签名,并将它们传递给一个执行其余操作的函数(可能是内联函数)。 - Rainer Blome
请更新您的答案以反映C++11,并回应@RainerBlome的评论。 - einpoklum
@RainerBlome:我们是同意的。RAISE_ERROR_STL宏是在C++11之前的,因此在那种情况下,它是完全合理的。我的理解(但我从未有过处理这些特定功能的机会)是,您可以在现代C++中使用可变参数模板(或宏?)更优雅地解决问题。 - paercebal
@einpoklum:“请更新您的答案以反映C++11并回应RainerBlome的评论。” 不。:-)……我相信,最好的情况是,我会添加一个现代C++的部分,其中包括减少或消除宏需求的替代实现,但问题仍然存在:宏很丑陋和邪恶,但当您需要做一些编译器不理解的事情时,您需要通过宏来完成。 - paercebal
即使使用C++11,您的宏定义仍然可以留给函数来完成:#include #include using namespace std; void trace(char const * file, int line, ostream & o) { cerr<(o).str().c_str()<这样,宏定义代码就会变得更短。 - Rainer Blome

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