C ++宏什么时候有益?

时间:2020-03-06 14:23:10  来源:igfitidea点击:

C预处理程序被C ++社区合理地恐惧和回避。内联函数,const和模板通常是对#define的更安全和更优的选择。

以下宏:

#define SUCCEEDED(hr) ((HRESULT)(hr) >= 0)

绝不优于安全类型:

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

但是宏确实有其位置,请列出我们在没有预处理器的情况下无法找到的宏的用途。

请把每个用例放在一个单独的答案中,以便可以将其投票。如果我们知道如何在没有预告员的情况下获得答案之一,请指出该答案的注释方式。

解决方案

当我们想更改程序流程(" return"," break"和" continue")时,函数中的代码与实际插入函数中的代码的行为有所不同。

#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.

通常,我最终会得到如下代码:

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) { ... }

在某些情况下,我将使用以下内容使我的生活更轻松:

#define APIARGS long a, long b, SomeCrazyClass c, long d, string e, string f, long double yx
int SomeAPICallbackMethod(APIARGS) { ... }

它附带了真正隐藏变量名的警告,这在较大的系统中可能是个问题,因此,并非总是正确的做法,只是在某些时候。

我们不能使用常规函数调用来使函数调用参数短路。例如:

#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

一种常见的用途是检测编译环境,例如,对于跨平台开发,我们可以编写一套针对linux的代码,而另一套针对Windows的代码,前提是我们还没有跨平台的库。

因此,在一个粗略的示例中,跨平台互斥锁可以具有

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

对于函数,当我们要显式忽略类型安全性时,它们很有用。例如上面和下面的许多示例来进行ASSERT。当然,就像许多C / C ++功能一样,我们可以用脚掌射击,但是该语言为我们提供了工具,并让我们决定了要做什么。

显然包括后卫

#ifndef MYHEADER_H
#define MYHEADER_H

...

#endif

当我们想用表达式制作字符串时,最好的例子是assert(#xx的值转换为字符串)。

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

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

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

并按以下方式使用它:

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

从C ++ 11开始,这已被基于范围的for循环所取代。

编译器可以拒绝内联请求。

宏将始终占有一席之地。

我发现有用的是#define DEBUG以进行调试跟踪-我们可以在调试问题时将其保留为1(甚至在整个开发周期中将其保留),然后在交付时将其关闭。

在编译时根据特定于Compiler / OS /硬件的行为进行决策时。

它允许我们将界面设置为Comppiler / OS /硬件特定功能。

#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

作为调试函数的包装器,它可以自动传递诸如__FILE LINE__等内容:

#ifdef ( DEBUG )
#define M_DebugLog( msg )  std::cout << __FILE__ << ":" << __LINE__ << ": " << msg
#else
#define M_DebugLog( msg )
#endif

内部条件编译,以克服编译器之间的差异问题:

#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

头文件防护需要宏。

还有其他需要宏的区域吗?数量不多(如果有)。

还有其他可从宏中受益的情况吗?是的!!!

我使用宏的一个地方是重复性很强的代码。例如,当包装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__预处理程序宏。

无论如何,正确使用宏非常有用。宏不是邪恶的-它们的滥用是邪恶的。

就像是

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);

而不是宏,我们所能获得的只是断言函数的行号打印到日志中,这将不太有用。

像UnitTest ++这样的C ++单元测试框架几乎都围绕预处理程序宏。几行单元测试代码扩展为一个类的层次结构,手动键入根本不会很有趣。没有像UnitTest ++这样的东西,它没有预处理器的魔力,我不知道我们如何有效地为C ++编写单元测试。

我们将__FILE____LINE__宏用于诊断目的,以进行信息丰富的异常抛出,捕获和记录,并在我们的质量检查基础架构中使用自动日志文件扫描程序。

例如,可以将抛出宏OUR_OWN_THROW与异常类型和该异常的构造函数参数(包括文本描述)一起使用。像这样:

OUR_OWN_THROW(InvalidOperationException, (L"Uninitialized foo!"));

这个宏当然会抛出" InvalidOperationException"异常,并将其描述作为构造函数参数,但是它还将向日志文件中写入一条消息,该日志文件由发生抛出的文件名和行号及其文本描述组成。引发的异常将获得一个ID,该ID也将被记录。如果该异常在代码中的其他任何地方被捕获,则将其标记为该异常,然后日志文件将指示该特定异常已被处理,因此不太可能导致以后记录的崩溃。未处理的异常可以通过我们的自动质量检查基础架构轻松获取。

我认为这个技巧是对预处理器的巧妙使用,无法通过函数进行仿真:

#define COMMENT COMMENT_SLASH(/)
#define COMMENT_SLASH(s) /##s

#if defined _DEBUG
#define DEBUG_ONLY
#else
#define DEBUG_ONLY COMMENT
#endif

然后,我们可以像这样使用它:

cout <<"Hello, World!" <<endl;
DEBUG_ONLY cout <<"This is outputed only in debug mode" <<endl;

我们还可以定义RELEASE_ONLY宏。

假设我们将忽略明显的东西,例如标题保护。

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

#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

其他时候,我们需要使用和#concatenation运算符来生成代码,例如为属性生成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;
}

(注意:代码从boost主页复制/粘贴)

(IMHO)比std :: for_each更好。

因此,宏总是有用的,因为它们不在正常的编译器规则之内。但是我发现,大多数时候我看到的是,它们实际上是C代码的剩余部分,从未被翻译成适当的C ++。

我们需要在Visual Studio中为资源标识符使用宏,因为资源编译器只能理解它们(即,它不适用于const或者enum)。

我偶尔使用宏,以便可以在一处定义信息,但是可以在代码的不同部分以不同的方式使用它。只是有点邪恶:)

例如,在" 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;

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

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

#include "field_list.h"

我们可以使用#defines来帮助调试和单元测试方案。例如,创建内存功能的特殊日志记录变体,并创建特殊的memlog_preinclude.h:

#define malloc memlog_malloc
#define calloc memlog calloc
#define free memlog_free

使用以下代码编译代码:

gcc -Imemlog_preinclude.h ...

memlog.o中的链接指向最终图像。现在,我们可以控制malloc等,可能是出于日志记录目的,或者是为了模拟单元测试的分配失败。

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

与当前线程中讨论的"首选"模板解决方案不同,我们可以将其用作常量表达式:

char src[23];
int dest[ARRAY_SIZE(src)];

我们可以将其实现为内联函数吗?

#define my_free(x) do { free(x); x = NULL; } while (0)

我们可以使用-D或者/ D选项在编译器命令行中使用#define常量。当为多个平台交叉编译相同的软件时,这通常很有用,因为我们可以让makefile控制为每个平台定义哪些常量。

在上一份工作中,我正在研究病毒扫描程序。为了使事情更容易调试,我到处都有很多日志记录,但是在这样的高需求应用程序中,函数调用的开销太昂贵了。因此,我想出了这个小宏,它仍然使我能够在客户站点的发行版上启用调试日志记录,而无需花费任何函数调用的费用,就可以检查调试标志并仅返回而不记录任何内容,或者启用,将进行日志记录...宏的定义如下:

#define dbgmsg(_FORMAT, ...)  if((debugmsg_flag  & 0x00000001) || (debugmsg_flag & 0x80000000))     { log_dbgmsg(_FORMAT, __VA_ARGS__);  }

由于日志函数中的VA_ARGS,对于这样的宏,这是一个很好的情况。

在此之前,我在高安全性应用程序中使用了一个宏,该宏需要告知用户他们没有正确的访问权限,并且可以告诉他们所需的标志。

宏定义为:

#define SECURITY_CHECK(lRequiredSecRoles) if(!DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, true)) return
#define SECURITY_CHECK_QUIET(lRequiredSecRoles) (DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, false))

然后,我们可以在整个UI上进行检查,如果我们还没有角色,它会告诉我们允许哪些角色执行我们尝试执行的操作。其中两个的原因是在某些地方返回值,而在其他地方从void函数返回...

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;
   }
...

无论如何,这就是我使用它们的方式,我不确定模板可以如何帮助它……除此之外,除非确实有必要,否则我会尽量避免使用它们。

如果我们有一个可以用于一堆东西的字段列表,例如定义一个结构,将该结构序列化为某种二进制格式或者从中以某种二进制格式进行序列化,进行数据库插入等操作,则可以(递归!)使用预处理器来避免重复字段列表。

这显然是可怕的。但是也许有时候比在多个地方更新一长列字段更好?我只使用过一次这项技术,而那一次非常有帮助。

当然,相同的总体思想已在具有适当反思的语言中广泛使用-只是对课程进行内部反思并依次在每个领域进行操作。在C预处理器中执行此操作非常脆弱,难以辨认,并且并非始终可移植。所以我有些害怕地提到了它。尽管如此,这里是...

(编辑:我现在看到这类似于@Andrew Johnson在9/18上所说的内容;但是递归地包含相同文件的想法使想法更进一步。)

// 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 )

害怕C预处理器就像害怕白炽灯泡,只是因为我们得到了荧光灯泡。是的,前者可以是{电力|程序员时间}效率低下。是的,我们可能(从字面上)被他们烧死。但是,如果我们妥善处理,他们就可以完成工作。

当我们对嵌入式系统编程时,C曾经是表单汇编程序之外的唯一选择。使用C ++在桌面上进行编程,然后切换到较小的嵌入式目标之后,我们将学会不再担心太多的裸露C功能(包括宏)的智能性,而只是试图找出可以从这些功能中获得的最佳安全使用。

亚历山大·斯蒂芬诺夫(Alexander Stepanov)说:

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.

大多:

  • 包括警卫
  • 条件编译
  • 报告(如" LINE"和" FILE"之类的预定义宏)
  • (很少)复制重复的代码模式。
  • 用我们竞争对手的代码。

我们可以在调试版本中启用其他日志记录,并在发布版本中禁用它,而无需进行布尔检查。因此,代替:

void Log::trace(const char *pszMsg) {
    if (!bDebugBuild) {
        return;
    }
    // Do the logging
}

...

log.trace("Inside MyFunction");

你可以有:

#ifdef _DEBUG
#define LOG_TRACE log.trace
#else
#define LOG_TRACE void
#endif

...

LOG_TRACE("Inside MyFunction");

如果未定义_DEBUG,则根本不会生成任何代码。程序将运行得更快,并且跟踪日志记录的文本不会被编译到可执行文件中。

有时最好将字符串常量定义为宏,因为使用字符串文字比使用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 ++"语言构造"则永远无法做到这一点。

例子:

使某些东西既是C标识符又是字符串

在C中将枚举类型的变量用作字符串的简单方法

增强预处理器元编程

我已经使用预处理器从无法在编译代码中使用浮点数的嵌入式系统中使用的浮点数计算定点数。将我们所有的数学都放在真实单位中是方便的,而不必在定点上考虑它们。

例子:

// 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 )

还有另一个foreach宏。 T:类型,c:容器,i:迭代器

#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)

用法(显示的概念,不是真实的):

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;
}

更好的实现方式:Google" BOOST_FOREACH"

可用的好文章:有条件的爱:FOREACH Redux(Eric Niebler)http://www.artima.com/cppsource/foreach.html

也许宏的最大用途是在与平台无关的开发中。
考虑一下与宏类型不一致的情况,我们可以简单地使用不同的头文件,例如:
--WIN_TYPES.H

typedef ...some struct

--POSIX_TYPES.h

typedef ...some another struct

--program.h

#ifdef WIN32
#define TYPES_H "WINTYPES.H"
#else 
#define TYPES_H "POSIX_TYPES.H"
#endif

#include TYPES_H

在我看来,它比以其他方式实现它更具可读性。