When are C++ macros beneficial?
C预处理器是由C++社区合理地害怕和回避的。在线性函数中,consts和template通常是比
以下宏:
1 | #define SUCCEEDED(hr) ((HRESULT)(hr) >= 0) |
绝不能优于类型安全:
1 | inline bool succeeded(int hr) { return hr >= 0; } |
号
但是宏确实有自己的位置,请列出在没有预处理器的情况下无法使用的宏的用途。
请将每个用例放在一个单独的答案中,以便投票表决,如果您知道如何在没有预评估者的情况下获得其中一个答案,请在该答案的评论中指出如何实现。
作为调试功能的包装器,自动传递如
1 2 3 4 5 | #ifdef ( DEBUG ) #define M_DebugLog( msg ) std::cout << __FILE__ <<":" << __LINE__ <<":" << msg #else #define M_DebugLog( msg ) #endif |
。
方法必须始终是完整的、可编译的代码;宏可能是代码片段。因此,可以定义foreach宏:
1 | #define foreach(list, index) for(index = 0; index < list.size(); index++) |
并以此方式使用:
1 2 | foreach(cookies, i) printf("Cookie: %s", cookies[i]); |
。
由于C++ 11,这是由范围为基础的循环取代。
头文件保护需要宏。
还有其他需要宏的地方吗?不多(如果有的话)。
是否还有其他从宏中受益的情况?对!!!!
我使用宏的一个地方是非常重复的代码。例如,当包装C++代码与其他接口(.NET、COM、Python等)一起使用时,我需要捕获不同类型的异常。我是这样做的:
1 2 3 4 5 6 7 8 9 10 | #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块,而是键入:
1 2 3 4 5 6 7 | void Foo() { try { ::mylib::Foo() } HANDLE_EXCEPTIONS } |
号
这也使得维护更加容易。如果必须添加一个新的异常类型,我只需要在一个地方添加它。
还有其他有用的例子:其中许多例子包括
总之,宏在正确使用时非常有用。宏不是邪恶的——它们的滥用是邪恶的。
主要是:
在条件编译中,为了克服编译器之间的差异问题:
1 2 3 4 5 6 7 8 | #ifdef ARE_WE_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 |
号
当您想从表达式中生成字符串时,最好的例子是
1 2 3 | #define ASSERT_THROW(condition) \ if (!(condition)) \ throw std::exception(#condition" is false"); |
。
字符串常量有时被更好地定义为宏,因为使用字符串文本比使用
例如,字符串文本可以很容易地连接起来。
1 2 3 4 | #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); |
如果使用了
1 2 3 | 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); |
号
当您想更改程序流(
1 2 3 4 5 6 | #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. |
明显的包括警卫
1 2 3 4 5 6 | #ifndef MYHEADER_H #define MYHEADER_H ... #endif |
。
不能使用常规函数调用执行函数调用参数的短路。例如:
1 2 3 4 5 6 | #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 |
。
C++的单元测试框架,类似于UnTest+++,非常适合于预处理器宏。几行单元测试代码扩展到一个类的层次结构中,手工输入一点也不有趣。没有像UnTest++这样的预处理器魔术,我不知道如何高效地编写C++的单元测试。
比如说,我们会忽略一些明显的事情,比如头球后卫。
有时,您希望生成需要由预编译器复制/粘贴的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #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) |
。
这使您能够对其进行编码:
1 | RAISE_ERROR_STL("Hello... The following values" << i <<" and" << j <<" are wrong") ; |
可以生成如下信息:
1 2 3 4 5 | Error Raised: ==================================== File : MyFile.cpp, line 225 Function : MyFunction(int, double) Message :"Hello... The following values 23 and 12 are wrong" |
。
请注意,将模板与宏混合可以得到更好的结果(即,自动将值与变量名并排生成)
例如,其他时候,您需要一些代码的文件和/或行来生成调试信息。以下是Visual C++的经典:
1 2 3 | #define WRNG_PRIVATE_STR2(z) #z #define WRNG_PRIVATE_STR1(x) WRNG_PRIVATE_STR2(x) #define WRNG __FILE__"("WRNG_PRIVATE_STR1(__LINE__)") : ------------ :" |
。
与以下代码相同:
1 | #pragma message(WRNG"Hello World") |
它生成的消息如下:
1 | C:\my_project\my_cpp_file.cpp (225) : ------------ Hello World |
。
其他时候,您需要使用和串联运算符生成代码,例如为属性生成getter和setter(对于非常有限的情况,通过)。
其他时候,如果通过函数使用,您将生成无法编译的代码,例如:
1 2 3 | #define MY_TRY try{ #define MY_CATCH } catch(...) { #define MY_END_TRY } |
可以用作
1 2 3 4 5 6 | MY_TRY doSomethingDangerous() ; MY_CATCH tryToRecoverEvenWithoutMeaningfullInfo() ; damnThoseMacros() ; MY_END_TRY |
。
(不过,我只看到这种代码正确使用过一次)
最后,但并非最不重要的是,著名的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #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; } |
号
(注:代码从Boost主页复制/粘贴)
这比
因此,宏总是有用的,因为它们超出了正常的编译器规则。但我发现大部分时间我都看到了,它们实际上是C代码的残余,从来没有翻译成合适的C++。
害怕C预处理器就像害怕白炽灯一样,因为我们有荧光灯。是的,前者可能是电编程时间效率低下。是的,你会被他们烧伤。但如果你处理得当,他们可以完成这项工作。
在编写嵌入式系统的程序时,C是除汇编程序之外唯一的选项。在桌面上用C++编程,然后切换到更小的嵌入式目标后,你就学会了不去担心如此多的裸C特征(包括宏)的"不雅",而只是想从这些特性中找出最好和安全的用法。
亚历山大斯蒂芬诺夫说:
When we program in C++ we should not be ashamed of its C heritage, but make
full use of it. The only problems with C++, and even the only problems with C, arise
when they themselves are not consistent with their own logic.
号
一些非常先进和有用的东西仍然可以使用预处理器(宏)来构建,而使用C++的"语言构造"(包括模板)是不可能做到的。
示例:
使某个东西同时成为C标识符和字符串
在C中使用枚举类型的变量作为字符串的简单方法
Boost预处理器元编程
代码重复。
看看增强预处理器库,这是一种元编程。在主题->动机中,你可以找到一个很好的例子。
我们使用
例如,一个抛出的宏
1 | OUR_OWN_THROW(InvalidOperationException, (L"Uninitialized foo!")); |
这个宏当然会抛出
我偶尔使用宏,这样我可以在一个地方定义信息,但在代码的不同部分以不同的方式使用它。只是有点邪恶:)
例如,在"field_list.h"中:
1 2 3 4 5 6 7 8 | /* * List of fields, names and values. */ FIELD(EXAMPLE1,"first example", 10) FIELD(EXAMPLE2,"second example", 96) FIELD(ANOTHER,"more stuff", 32) ... #undef FIELD |
号
然后,对于公共枚举,可以定义为仅使用名称:
1 2 3 4 5 6 7 8 9 | #define FIELD(name, desc, value) FIELD_ ## name, typedef field_ { #include"field_list.h" FIELD_MAX } field_en; |
号
在private init函数中,所有字段都可用于用数据填充表:
1 2 3 4 5 | #define FIELD(name, desc, value) \ table[FIELD_ ## name].desc = desc; \ table[FIELD_ ## name].value = value; #include"field_list.h" |
号
一个常见的用途是检测编译环境,对于跨平台开发,您可以为Linux编写一组代码,也可以在没有跨平台库的情况下为Windows编写另一组代码。
因此,在一个粗略的例子中,跨平台互斥体可以
1 2 3 4 5 6 7 8 9 | void lock() { #ifdef WIN32 EnterCriticalSection(...) #endif #ifdef POSIX pthread_mutex_lock(...) #endif } |
对于函数,当您想要显式地忽略类型安全性时,它们是有用的。比如上面和下面做断言的很多例子。当然,就像很多C/C++特征一样,你可以用脚射击自己,但是语言给了你工具,让你决定做什么。
有点像
1 2 | void debugAssert(bool val, const char* file, int lineNumber); #define assert(x) debugAssert(x,__FILE__,__LINE__); |
号
这样你就可以
1 | assert(n == true); |
号
如果n为假,则将问题的源文件名和行号打印到日志中。
如果使用普通函数调用,例如
1 | void assert(bool val); |
号
与宏不同,您所能得到的只是将断言函数的行号打印到日志中,这样就不那么有用了。
1 | #define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0]) |
号
与当前线程中讨论的"首选"模板解决方案不同,您可以将其用作常量表达式:
1 2 | char src[23]; int dest[ARRAY_SIZE(src)]; |
号
我使用宏轻松定义异常:
1 | DEF_EXCEPTION(RessourceNotFound,"Ressource not found") |
其中def_例外
1 2 3 4 5 6 7 8 | #define DEF_EXCEPTION(A, B) class A : public exception\ {\ public:\ virtual const char* what() const throw()\ {\ return B;\ };\ }\ |
。
您可以使用定义来帮助调试和单元测试场景。例如,创建内存函数的特殊日志变量,并创建一个特殊的memlog_preinclude.h:
1 2 3 | #define malloc memlog_malloc #define calloc memlog calloc #define free memlog_free |
号
编译代码时使用:
1 | gcc -Imemlog_preinclude.h ... |
号
memlog.o中指向最终图像的链接。现在您可以控制malloc等,可能是为了日志记录,或者为单元测试模拟分配失败。
当您在编译时决定编译器/OS/硬件特定的行为时。
它允许您建立与comppiler/os/硬件特定功能的接口。
1 2 3 4 5 6 7 8 9 10 | #if defined(MY_OS1) && defined(MY_HARDWARE1) #define MY_ACTION(a,b,c) doSothing_OS1HW1(a,b,c);} #elif define(MY_OS1) && defined(MY_HARDWARE2) #define MY_ACTION(a,b,c) doSomthing_OS1HW2(a,b,c);} #elif define(MY_SUPER_OS) /* On this hardware it is a null operation */ #define MY_ACTION(a,b,c) #else #error "PLEASE DEFINE MY_ACTION() for this Compiler/OS/HArdware configuration" #endif |
似乎到目前为止,人们只是间接地提到了虚拟货币:
当编写泛型C++ 03代码时,需要一个可变数量的(通用)参数,可以使用宏而不是模板。
1 2 3 4 5 6 7 | #define CALL_RETURN_WRAPPER(FnType, FName, ...) \ if( FnType theFunction = get_op_from_name(FName) ) { \ return theFunction(__VA_ARGS__); \ } else { \ throw invalid_function_name(FName); \ } \ /**/ |
号
注:一般来说,名称检查/抛出也可以合并到假设的
一旦我们得到了带有C++ 11的可变模板,我们就可以用模板来"适当"地解决这个问题。
也许宏最常用于独立于平台的开发。考虑类型不一致的情况-使用宏,您可以简单地使用不同的头文件-例如:--赢的类型.h
1 | typedef ...some struct |
。
--posix_类型.h
1 | typedef ...some another struct |
。
--程序.h
1 2 3 4 5 6 7 | #ifdef WIN32 #define TYPES_H"WINTYPES.H" #else #define TYPES_H"POSIX_TYPES.H" #endif #include TYPES_H |
在我看来,它比用其他方式实现它更具可读性。
还有一个foreach宏。T:类型,C:容器,I:迭代器
1 2 | #define foreach(T, c, i) for(T::iterator i=(c).begin(); i!=(c).end(); ++i) #define foreach_const(T, c, i) for(T::const_iterator i=(c).begin(); i!=(c).end(); ++i) |
。
用法(概念展示,非真实):
1 2 3 4 5 6 7 8 9 10 11 12 13 | void MultiplyEveryElementInList(std::list<int>& ints, int mul) { foreach(std::list<int>, ints, i) (*i) *= mul; } int GetSumOfList(const std::list<int>& ints) { int ret = 0; foreach_const(std::list<int>, ints, i) ret += *i; return ret; } |
更好的实现:谷歌"Boost-ForEach"
好文章:ConditionalLove:ForEach Redux(Eric Niebler)http://www.artima.com/cppsource/ForEach.html
编译器可以拒绝您的内联请求。
宏总是有自己的位置。
我发现一些有用的东西是为调试跟踪定义调试——您可以在调试问题时(甚至在整个开发周期中)将其保留为1,然后在发布时将其关闭。
在上一份工作中,我在做病毒扫描。为了让我调试更容易,我有很多日志记录卡在各处,但在这样一个高需求的应用程序中,函数调用的费用太高了。所以,我想到了这个小宏,它仍然允许我在客户站点上启用发布版本的调试日志记录,而不需要花费函数调用的费用,它会检查调试标志,然后返回而不记录任何内容,或者如果启用,它会做日志记录…宏定义如下:
1 | #define dbgmsg(_FORMAT, ...) if((debugmsg_flag & 0x00000001) || (debugmsg_flag & 0x80000000)) { log_dbgmsg(_FORMAT, __VA_ARGS__); } |
由于日志函数中有va_参数,对于这样的宏来说,这是一个很好的例子。
在此之前,我在一个高安全性的应用程序中使用了一个宏,它需要告诉用户他们没有正确的访问权限,并且它会告诉他们需要什么标志。
宏定义为:
1 2 | #define SECURITY_CHECK(lRequiredSecRoles) if(!DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, true)) return #define SECURITY_CHECK_QUIET(lRequiredSecRoles) (DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, false)) |
号
然后,我们可以将检查撒在整个UI上,它将告诉您哪些角色可以执行您尝试执行的操作,如果您还没有该角色的话。其中两个的原因是在某些地方返回一个值,而在其他地方从一个空函数返回…
1 2 3 4 5 6 7 8 9 10 | SECURITY_CHECK(ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR); LRESULT CAddPerson1::OnWizardNext() { if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_EMPLOYEE) { SECURITY_CHECK(ROLE_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD ) -1; } else if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_CONTINGENT) { SECURITY_CHECK(ROLE_CONTINGENT_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR) -1; } ... |
不管怎样,这就是我使用它们的方式,我不确定这对模板有什么帮助…除此之外,我尽量避开它们,除非真的有必要。
如果您有一个字段列表,可以用于许多事情,例如定义结构、将该结构序列化为某种二进制格式或从某种二进制格式序列化、执行数据库插入等,那么您可以(递归地!)使用预处理器避免重复域列表。
这是公认的可怕。但有时可能比在多个地方更新一长串字段要好?我只用过一次这种技巧,这一次非常有用。
当然,相同的一般思想在语言中被广泛地使用,并有适当的反映——只需对类进行instrospect并依次对每个字段进行操作。在C预处理器中执行该操作是脆弱的、难以辨认的,并且不总是可移植的。所以我有些不安地提到它。尽管如此,这是…
(编辑:我现在看到这与@andrew johnson在9/18中所说的类似;但是递归地包含相同文件的想法需要更进一步。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // file foo.h, defines class Foo and various members on it without ever repeating the // list of fields. #if defined( FIELD_LIST ) // here's the actual list of fields in the class. If FIELD_LIST is defined, we're at // the 3rd level of inclusion and somebody wants to actually use the field list. In order // to do so, they will have defined the macros STRING and INT before including us. STRING( fooString ) INT( barInt ) #else // defined( FIELD_LIST ) #if !defined(FOO_H) #define FOO_H #define DEFINE_STRUCT // recursively include this same file to define class Foo #include"foo.h" #undef DEFINE_STRUCT #define DEFINE_CLEAR // recursively include this same file to define method Foo::clear #include"foo.h" #undef DEFINE_CLEAR // etc ... many more interesting examples like serialization #else // defined(FOO_H) // from here on, we know that FOO_H was defined, in other words we're at the second level of // recursive inclusion, and the file is being used to make some particular // use of the field list, for example defining the class or a single method of it #if defined( DEFINE_STRUCT ) #define STRING(a) std::string a; #define INT(a) long a; class Foo { public: #define FIELD_LIST // recursively include the same file (for the third time!) to get fields // This is going to translate into: // std::string fooString; // int barInt; #include"foo.h" #endif void clear(); }; #undef STRING #undef INT #endif // defined(DEFINE_STRUCT) #if defined( DEFINE_ZERO ) #define STRING(a) a =""; #define INT(a) a = 0; #define FIELD_LIST void Foo::clear() { // recursively include the same file (for the third time!) to get fields. // This is going to translate into: // fooString=""; // barInt=0; #include"foo.h" #undef STRING #undef int } #endif // defined( DEFINE_ZERO ) // etc... #endif // end else clause for defined( FOO_H ) #endif // end else clause for defined( FIELD_LIST ) |
。
使用
我认为这个技巧是对不能用函数模拟的预处理器的巧妙使用:
1 2 3 4 5 6 7 8 | #define COMMENT COMMENT_SLASH(/) #define COMMENT_SLASH(s) /##s #if defined _DEBUG #define DEBUG_ONLY #else #define DEBUG_ONLY COMMENT #endif |
号
然后你可以这样使用它:
1 2 | cout <<"Hello, World!" <<endl; DEBUG_ONLY cout <<"This is outputed only in debug mode" <<endl; |
您还可以定义一个仅限发布的宏。
我使用预处理器从嵌入式系统中不能在编译代码中使用浮点的浮点值计算定点数字。把你所有的数学都放在现实世界的单位中是很方便的,而不必用固定点来考虑它们。
例子:
1 2 3 4 5 6 7 8 9 | // TICKS_PER_UNIT is defined in floating point to allow the conversions to compute during compile-time. #define TICKS_PER_UNIT 1024.0 // NOTE: The TICKS_PER_x_MS will produce constants in the preprocessor. The (long) cast will // guarantee there are no floating point values in the embedded code and will produce a warning // if the constant is larger than the data type being stored to. // Adding 0.5 sec to the calculation forces rounding instead of truncation. #define TICKS_PER_1_MS( ms ) (long)( ( ( ms * TICKS_PER_UNIT ) / 1000 ) + 0.5 ) |
您可以在调试版本中启用额外的日志记录,并在没有布尔检查开销的情况下为发布版本禁用它。所以,不是:
1 2 3 4 5 6 7 8 9 10 | void Log::trace(const char *pszMsg) { if (!bDebugBuild) { return; } // Do the logging } ... log.trace("Inside MyFunction"); |
您可以有:
1 2 3 4 5 6 7 8 9 | #ifdef _DEBUG #define LOG_TRACE log.trace #else #define LOG_TRACE void #endif ... LOG_TRACE("Inside MyFunction"); |
。
如果没有定义调试,则不会生成任何代码。您的程序将运行得更快,跟踪日志记录的文本将不会编译到可执行文件中。
在Visual Studio中,您需要一个用于资源标识符的宏,因为资源编译器只理解这些宏(即,它不适用于const或enum)。
你能把它作为一个内联函数来实现吗?
1 | #define my_free(x) do { free(x); x = NULL; } while (0) |
号
1 2 3 4 5 6 7 8 9 10 | #define COLUMNS(A,B) [(B) - (A) + 1] struct { char firstName COLUMNS( 1, 30); char lastName COLUMNS( 31, 60); char address1 COLUMNS( 61, 90); char address2 COLUMNS( 91, 120); char city COLUMNS(121, 150); }; |
。
宏可用于模拟switch语句的语法:
1 2 3 4 5 6 | switch(x) { case val1: do_stuff(); break; case val2: do_other_stuff(); case val3: yet_more_stuff(); default: something_else(); } |
号
对于非整数值类型。在这个问题中:
在Switter语句中使用字符串——我们在C++ 17中的立场是什么?
你会发现一些关于lambda的方法的答案,但不幸的是,正是宏让我们最接近:
1 2 3 4 5 6 | SWITCH(x) CASE val1 do_stuff(); break; CASE val2 do_other_stuff(); CASE val3 yet_more_stuff(); DEFAULT something_else(); END |
号
通常情况下,我会得到如下代码:
1 2 3 | int SomeAPICallbackMethod(long a, long b, SomeCrazyClass c, long d, string e, string f, long double yx) { ... } int AnotherCallback(long a, long b, SomeCrazyClass c, long d, string e, string f, long double yx) { ... } int YetAnotherCallback(long a, long b, SomeCrazyClass c, long d, string e, string f, long double yx) { ... } |
号
在某些情况下,我会使用以下方法来简化我的生活:
1 2 | #define APIARGS long a, long b, SomeCrazyClass c, long d, string e, string f, long double yx int SomeAPICallbackMethod(APIARGS) { ... } |
它附带了真正隐藏变量名的警告,这在较大的系统中可能是一个问题,所以这并不总是正确的事情,只是有时。