c/c++ 宏

宏替换是 C/C++ 系列语言的技术特色,C/C++ 语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等。

常用的宏替换类型:

一、宏常量

在ACM等算法竞赛中,经常会把数组的最大下标通过宏定义的方法给出,以方便调试,例如:

#define MAX 1000

int array[MAX][MAX]

……
for(int i = 0; i < MAX; i++)
……

将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著作《Effective C++》中,这种做法却并不提倡,书中更加推荐以 const 常量来代替宏常量。因为在进行词法分析时,宏的引用已经被其实际内容替换,因此宏名不会出现在符号表中。所以一旦出错,看到的将是一个无意义的数字,比如上文中的 1000,而不是一个有意义的名称,如上文中的 MAX。而 const 在符号表中会有自己的位置,因此出错时可以看到更加有意义的错误提示。

二、用于条件编译标识的宏

#define 常与 #ifdef or #ifndef or defined 指令配合使用,用于条件编译。

#ifndef _HEADER_INC_
#define _HEADER_INC_
……
……
#endif

// 这种宏标记在头文件中十分常见,用于防止头文件被反复包含。
// 应该养成习惯在每个头文件中都添加这种标记。还有一种用于条件编译的用法。

#ifdef DEBUG
printf("{“}Debug information\n");
#endif

通过 DEBUG 宏,我们可以在代码调试的过程中输出辅助调试的信息。当 DEBUG 宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果。   在条件编译时建议使用 #if defined 和 #if !defined 来代替使用 #ifdef or #ifndef,因为前者更方便处理多分支的情况与较复杂条件表达式的情况。

#ifdef or #ifndef 只能处理两个分支:#ifdef or #ifndef, #else, #endfi;

#if defined 和 #if !defined 可以处理多分支的情况:#if defined or #if !defined, #elif defined, #else, #endif。

#ifdef只能判断是否定义,但是 #if defined 可以判断复杂的表达式的值是否为真。

#if defined(OS_HPUX)&&(defined(HPUX_11_11)|| defined(HPUX_11_23) 
// for HP-UX 11.11 and 11.23 
#elif defined(OS_HPUX) && defined(HPUX_11_31 
// for HP-UX 11.31 
#elif defined(OS_AIX) 
// for AIX 
#else 
… 
#endif

条件编译时,如果一个文件中太多条件编译的代码,有些编辑器的智能感知可能都不能很好地解析,还是保持代码越简单越好。对于函数级别的条件编译主要有两种实现方式: 

三、宏函数

宏函数的语法有以下特点:

  1. 避免函数调用,提高程序效率

常用的就是最大值与最小值的判断函数,由于函数内容并不多,如果定义为函数在调用比较频繁的场合会明显降低程序的效率,其实宏是用空间效率换取了时间效率。如取两个值的最大值: 

#define MAX(a,b) ((a)<(b) ? (b) : (a))

// 定义为函数: 
inline int Max(int a, int b)
{
 return a < b ? b : a;
}

// 定义为模板: 
template <typename T> 
inline T TMax(T a, T b)
{
 return a < b ? b : a ;
}

使用宏函数的优点有两个:

需要注意的是,由于宏的本质是直接的文本替换,所以在宏函数的“函数体”内都要把参数使用括号括起来,防止参数是表达式时造成语法错误或结果错误,如:

#define MIN(a, b) b < a ? b : a 
#define SUM(a, b) a + b 

cout << MIN(3, 5) << endl; // 语法错误:cout << b < a ? b : a << endl; 
int c = SUM(a, b) * 2;     // c的期望值:16,实际值:13
  1. 引用编译期数据

上述的这些作用虽然使用宏函数可以取得更好的性能,但如果从功能上讲完全可以不使用宏函数,而使用模板函数或普通函数实现,但还有些时候只能通过宏实现。例如,程序中在执行某些操作时可能会失败,此时要打印出失败的代码位置,只能使用宏实现。

#define SHOW_CODE_LOCATION() cout << __FILE__ << ':' << __LINE__ << '\n'

if( 0 != rename("oldFileName", "newFileName") )
{
 cout << "failed to move file" << endl;
 SHOW_CODE_LOCATION();
}

虽然宏是简单的替换,所以在调用宏函数 SHOW_CODE_LOCATION 时,分号可以直接写到定义里,也可以写到调用处,但最好还是写到调用处,看起来更像是调用了函数,否则看着代码不伦不类,如:

#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n' 

if( 0 != rename("oldFileName", "newFileName") )
{ 
 cout<<"failed to move file"<<endl;
 SHOW_CODE_LOCATION()
}
  1. do-while 的妙用

do-while 循环控制语句的特点就是循环体内的语句至少会被执行一次,如果 while(…) 内的条件始终为 0 时,循环体内的语句就会被执行且只被执行一次,这样的执行效果与直接使用循环体内的代码相同,但这们会得到更多的益处。

#define SWAP_INT(a, b) do
{\
 int tmp = a; \
 a = b; \
 b = tmp; \
}while(0)

int main( void ) 
{ 
 int x = 3, y = 4;
 if( x > y )
 {
  SWAP_INT(x, y);
 }
 return 0;
}

通过 do-while 代码块的宏定义我们不仅可以把 SWAP_INT 像函数一样用,而且还有优点:

其实我们定义的 SWAP_INT(a, b) 相当于定义了引用参数或指针参数的函数,因为它可以改变实参的值。在 C++0X 中有了 decltype 关键词,这种优势就更显示了,因为在宏中使用了局部变量必须确定变量的类型,所以这个宏只能用于交换 int 型的变量值,如果换作其它类型则还必须定义新的宏,如 SWAP_FLOAT、SWAP_CHAR 等,而通过 decltype,我们就可以定义一个万能的宏。

#include <iostream> 
using namespace std;

#define SWAP(a, b) do
{ \
 decltype(a) tmp = a; \
 a = b; \
 b = tmp; \
}while(0)

int main( void ) 
{ 
 int a = 1, b = 2; 
 float f1 = 1.1f, f2 = 2.2f; 
 SWAP(a, b); 
 SWAP(f1,f2); 
 return 0; 
}

通过宏实现的 SWAP “函数” 要比使用指针参数效率还要高,因为它连指针参数都不用传递而是使用直接代码,对于一些效率要求比较明显的场合,宏还是首选。

四、取消宏定义

#undef 指令用于取消前面用 #define 定义的宏,取消后就可以重新定义宏。该指令用的并不多,因为过多的 #undef 会使代码维护起来非常困难,一般也只用于配置文件中,用来清除一些 #define 的开关,保证宏定义的唯一性。

// config.h 
#undef HAS_OPEN_SSL 
#undef HAS_ZLIB 
#if defined(HAS_OPEN_SSL) 
… 
#endif 
#if defined(HAS_ZLIB) 
… 
#endif

将对该头文件的引用放到所有代码文件的第一行,就可以保证 HAS_OPEN_SSL 没有被定义,即使是在编译选项里定义过一宏,也会被 #undef 指令取消,这样使得 config.h 就是唯一一处放置条件编译开关的地方,更有利于维护。

五、注意事项

  1. 普通宏定义
  1. 带参宏定义

六、关于 # 和 ##

# – 字符化操作

作用是将宏定义参数不经任何扩展地转换成字符串常量(Stringfication),所谓拓展包括:

#define WARN_IF(EXP)    \
    do{ if (EXP)    \
          fprintf(stderr, "Warning: " #EXP "\n"); }   \
    while(0)

那么实际使用中会出现下面所示的替换过程:

WARN_IF (divider == 0);

// 被替换为

do {
    if (divider == 0)
      fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);

这样每次divider(除数)为0的时候便会在标准错误流上输出一个提示信息。

## 标记连接操作

## 的作用是在宏定义中,用来将两个 Token 连接为一个 Token。注意这里连接的对象是 Token 就行,而不一定是宏的变量。需要注意:

比如你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:

struct command
{
 char * name;
 void (*function) (void);
};

#define COMMAND(NAME) { NAME, NAME##_command }

// 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:

struct command commands[] = {
 COMMAND(quit),
 COMMAND(help),
 ...
}

COMMAND 宏在这里充当一个代码生成器的作用,这样可以在一定程度上减少代码密度,间接地也可以减少不留心所造成的错误。我们还可以 n 个 ## 符号连接 n+1 个 Token,这个特性也是 # 符号所不具备的。比如:

#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d

typedef struct _record_type LINK_MULTIPLE(name, company, position, salary);

// 这里这个语句将展开为:
//  typedef struct _record_type name_company_position_salary;

七、关于 … 的使用

在 C 宏中称为 Variadic Macro,也就是变参宏。比如:

#define myprintf(templt, ...) fprintf(stderr, templt, __VA_ARGS__)

// 或者

#define myprintf(templt, args...) fprintf(stderr, templt, args)

第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。

第二个宏中,我们显式地命名变参为 args,那么我们在宏定义中就可以用 args 来代指变参了。

同 C 语言的 stdcall 一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数 templt 时,C 标准要求我们必须写成:

myprintf(templt,);

这时的替换过程为:

myprintf("Error!\n",);

// 替换为:
 
fprintf(stderr, "Error!\n",);

这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP 提供的解决方法允许上面的宏调用写成:

myprintf(templt);

而它将会被通过替换变成:

fprintf(stderr,"Error!\n",);

很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99 和 GNU CPP 都支持下面的宏定义方式:

#define myprintf(templt, ...) fprintf(stderr, templt, ##__VAR_ARGS__)

这时,## 这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:

myprintf(templt);

// 被转化为:

fprintf(stderr,templt);

这样如果 templt 合法,将不会产生编译错误。

八、关于 ## 消除__VAR_ARGS__前面的逗号

# 与 ## 在宏定义中的–宏展开

#include <stdio.h>

#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)

int main()
{
  printf("%s\n", g(f(1,2))); // f(1,2)
  printf("%s\n", h(f(1,2))); // 12
  return 0;
}

宏展开时:

九、caffe 源码程序入口宏

#define RegisterBrewFunction(func) \
namespace { \
class __Registerer_##func { \
 public: /* NOLINT */ \
  __Registerer_##func() { \
    g_brew_map[#func] = &func; \
  } \
}; \
__Registerer_##func g_registerer_##func; \

定义了一个注册函数,把函数的指针保存到一个容器中。容器定义如下

typedef int (*BrewFunction)();
typedef std::map<caffe::string, BrewFunction> BrewMap;
BrewMap g_brew_map;

主函数入口为:

  gflags::SetUsageMessage("command line brew\n"
      "usage: caffe <command> <args>\n\n"
      "commands:\n"
      "  train           train or finetune a model\n"
      "  test            score a model\n"
      "  device_query    show GPU diagnostic information\n"
      "  time            benchmark model execution time");
  // Run tool or show usage.
  caffe::GlobalInit(&argc, &argv);
  if (argc == 2) {
    return GetBrewFunction(caffe::string(argv[1]))();

从这个函数中把四种参数传入,函数定义如下

// input : caffe::string(train,test...)
// output : BrewFunction function pointer
static BrewFunction GetBrewFunction(const caffe::string& name) {
  // use map type to check name appears frequency
  if (g_brew_map.count(name)) {
    return g_brew_map[name];
  } else {
    // if do not find the specified name,the output error
    LOG(ERROR) << "Available caffe actions:";
	// Traverse the entire map, output an error message
    for (BrewMap::iterator it = g_brew_map.begin();
         it != g_brew_map.end(); ++it) {
      LOG(ERROR) << "\t" << it->first;
    }
    LOG(FATAL) << "Unknown action: " << name;
    return NULL;  // not reachable, just to suppress old compiler warnings.
  }
}

从这里可以看出,传入参数,看看这个参数和 g_brew_map 是这个容器里面的吗,是的话,然后函数指针,利用该函数指针来调用相应的函数。

Table of Contents