在C/C++项目中合理使用宏详解

C++项目中常使用宏来做跨平台、功能实现隔离、变量定义的功能,这篇文章来讨论下是否所有情况下都适合用宏

小D的故事

程序员小D接到一个任务,需要给同事A提供一个复杂公式的实现。输入为一组参数,输出一个计算结果。

大致如下:

double computeSomeThing(double paramA,double paramB,double paramC);

小D很快完成了。过了几天同事A又来找他,说现在需要提升该函数的性能,建议改为在float类型上,用一些SIMD指令。且同事A表示不是很愿意修改接口。于是小D在考虑以下两点后决定用一个宏把原来double的实现和float的实现分开来。

1、上层需求变动性比较大,说不定哪天又要用double了。所以还是保留double类型的实现

2、用宏把两份代码隔开来,互相不影响比较省事

于是代码就变成了这样:

double computeSomeThing(double paramA,double paramB,double paramC)
{
 #ifdef _USE_DOUBLE_
 // do something in double
 #else
 // convert dobule to float
 // do something in float
 // convert float to double
 #endif
}

同事A很满意,因为他只要替换一下.so或.a即可,代码层不需要改动。于是和小D合作开发了很多这样的函数,并且都有float和double两种实现。在对性能要求高的时候要求小D提供float版本;性能要求低,精度要求的时候要求小D提供double版本。

此时小D会在出库的时候感到一丝不方便。第一,版本号中需要区分float和double版本。

第二,因为用宏隔开,切换两个版本的时候需要重新编译,而代码量很多所以编译时间很长,但这些都是能克服的。

直到有一天同事小B的模块也需要这个库,并且小A和小B的模块要组合起来给小C用,最要命的是小A和小B的模块分别要用float版本和double版本。所以此时应该提供float版本so还是提供double版本so呢。

问题分析

在上面的场景中,小D作为一个基础库的提供者不应该因为同事不愿意修改接口或者图方便用宏去隔离功能,使得一个接口有了二义性。比较合适的一种做法是,再提供一个控制选择变量,来选择用哪种实现,即允许运行时决定用float还是double版本。

double computeSomeThing(double paramA,double paramB,double paramC,bool isFast);

或者小A就是不愿意改接口(考虑实际项目中,接口参数复杂且调用分散在各处),那么也可以通过增加接口实现。

double computeSomeThing(double paramA,double paramB,double paramC);

void setFast(bool isFast);

下面的情况用宏做隔离就是比较合理的选择。

比如一套代码要分别运行在linux和windows上,依赖的头文件、部分基础函数接口都是有区别的。此时用宏去隔离就比较合理。因为这两个版本在运行时永远不会同时出现。除了平台差异性外,版本管理也可以用宏来做隔离。

比如opencl 1.2和opencl 2.0版本相比较的话,2.0版本中新增了SVM相关的接口。当一个opencl程序未来可能运行在1.2版本的设备和2.0版本的设备上时。

可以用宏来选择是否屏蔽掉SVM接口。因为2.0的接口运行在1.2的设备上时,无法从环境中获取2.0新增的接口实现导致程序跑不起来(1.2的相关so中没有SVM函数实现)。

不过这个问题用宏来处理也不是最优的,使用dlopen可以有更灵活的实现。

总结

对于做基础库提供给很多人使用的同学,当用宏隔开的代码有可能会同时运行在一个环境时建议改为运行时选择走哪条分支。但肯定互相不兼容的时候就放心的用宏吧,比如跨操作系统。

另外提一下,对于有很多代码的大项目用宏的时候也要慎重考虑一下,不要动不动就用宏去做一些功能开关,因为编译时间太长是很影响效率的。

比如有以下宏定义:

#define _OPEN_LOG_
#ifdef _OPEN_LOG_
 #define LOG_PRINT(...) printf(...)
#else
 #define LOG_PRINT(...)
#endif

开发阶段代码中到处插着LOG_PRINT的使用,发布时关闭打印又是一波整个项目重新编译。再多来几个这种功能,每次切换又是整个项目重新编译,非常烦人。可以用函数指针代替:

typedef void (*LogPrint)(const char * pstrMsg);
LogPrint g_LogPrint;
void LogPrint_Imp(const char *pstrMsg)
{
 printf("%s\n",pstrMsg);
 return;
}
void LogPrint_Empty(const char *pstrMsg)
{
 return;
}
int main(int argc,char **argv)
{
 // 此处对日志功能进行开关
 g_LogPrint = LogPrint_Imp ;
 //g_LogPrint = LogPrint_Empty ;
 // .....
}
void someFun()
{
 g_LogPrint("in someFun"); //到底打印还是不打印,运行时决定
}

在这个例子中,关闭日志时编译器只会对main函数所在的文件进行重新编译,就不用费时费力的重新编译整个项目了。而且还可以把g_LogPrint的赋值的行为通过接口开放到上层,由调用者决定是否需要打开log。

再举个例子,有些人喜欢项目中各个代码模块中用到的参数提到一个头文件中,然后各个.c都包含这个头文件。就像这样:

// GobalParam.h
#ifndef XX_XX
#define XX_XX
#define DETECTION_MAX 100
#define INPUT_WIDTH_MAX 4096
#define INPUT_HEIGHT_MAX 4096
// 诸如此类很多宏
#endif

我个人觉得下面这种实现更好

// GobalParam.h
#ifndef XX_XX
#define XX_XX
extern const int DETECTION_MAX;
extern const int INPUT_WIDTH_MAX ;
extern const int INPUT_HEIGHT_MAX ;
// 在某个.c或.cpp中赋值 :const int INPUT_HEIGHT_MAX = 100;
#endif

这样你对某个参数修改的时候,就不用眼巴巴的等着所有包含此头文件的编译模块重新编译了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方欢迎留言讨论,望不吝赐教。

(0)

相关推荐

  • C/C++宏替换实现详解

    基本形式 #define name replacement_text 通常情况下,#define 指令占一行,替换文本是 define 指令行尾部的所有剩余部分,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符 ``. 宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本.例: #define max(A, B) ((A) > (B) ? (A) : (B)) 宏展开中的陷阱 仔细考虑一下 max 的展开式,其中的表达式会被计算两次,因此如果表达式中包含自增运

  • C/C++中宏/Macro的深入讲解

    前言 宏(Macro)本质上就是代码片段,通过别名来使用.在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分. C/C++ 代码编译过程 - 图片来自 ntu.edu.sg 根据用法的不同,分两种,Object-like 和 Function-like.前者用于 Object 对象,后者用于函数方法. C/C++ 代码编译过程中,可通过相应参数来获取到各编译步骤中的产出,比如想看被预处理编译之后的宏,使用 gcc 使加上 -E 参数. $ gcc

  • C/C++语言宏定义使用实例详解

     C/C++语言宏定义使用实例详解 1. #ifndef 防止头文件重定义 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成 一个可执行文件时,就会出现大量"重定义"的错误.在头文件中实用#ifndef #define #endif能避免头文件的重定义. 方法:例如要编写头文件test.h 在头文件开头写上两行: #ifndef TEST_H #define TEST_H //一般是文件名的大写 头文件结尾写上一行: #endif 这样一个工程文件里同时

  • 详解C/C++中const关键字的用法及其与宏常量的比较

    1.const关键字的性质 简单来说:const关键字修饰的变量具有常属性. 即它所修饰的变量不能被修改. 2.修饰局部变量 const int a = 10; int const b = 20; 这两种写法是等价的,都是表示变量的值不能被改变,需要注意的是,用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了,而且编译器也不允许不赋初值的写法: 在C++中不赋初值的表达一写出来,编译器即报错,且编译不通过. 在C中不赋初值的表达写出来时不报错,编译时只有警告,编译可以通过.而

  • C/C++中宏定义(#define)

    #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但学生在学习时往往不能 理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,使得程序的运行与预期的目的不一致,或者在读别人写的程序时,把运行结果理解错误,这对 C语言的学习很不利. 宏的定义在程序中是非常有用的,但是使用不当,就会给自身造成很大的困扰.通常这种困扰为:宏使用在计算方面. 本例子主要是在宏的计算方面,很多时候,大家都知道定义一个计算的宏,对于编译和编程

  • 如何区分C++中的inline和#define宏

    (1)什么是内联函数? 内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内. (2)为什么要引入内联函数? 当然,引入内联函数的主要目的是:解决程序中函数调用的效率问题. 另外,前面我们讲到了宏,里面有这么一个例子: #define ABS(x) ((x)>0? (x):-(x)) 当++i出现时,宏就会歪曲我们的意思,换句话说就是:宏的定义很容易产生二意性. (3)为什么inline能取代宏? 1. inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替

  • 在C/C++项目中合理使用宏详解

    C++项目中常使用宏来做跨平台.功能实现隔离.变量定义的功能,这篇文章来讨论下是否所有情况下都适合用宏 小D的故事 程序员小D接到一个任务,需要给同事A提供一个复杂公式的实现.输入为一组参数,输出一个计算结果. 大致如下: double computeSomeThing(double paramA,double paramB,double paramC); 小D很快完成了.过了几天同事A又来找他,说现在需要提升该函数的性能,建议改为在float类型上,用一些SIMD指令.且同事A表示不是很愿意修

  • bing Map 在vue项目中的使用详解

    写在最前面 拥有全球数据库国内好像就只有百度地图有,高德.搜狗.腾讯的都不行,但是由于百度地图的数据更新不及时,所以在做相关项目要用到国外数据的时候,最好还是推荐使用bingMap. bing Map 使用教程(基础) 参考文档:bing Map 官方教程 bing Map 初始化 引入bing map资源 <script type='text/javascript' src='http://www.bing.com/api/maps/mapcontrol?callback=GetMap&k

  • MVVM和MVVMLight框架介绍及在项目中的使用详解

    一.MVVM 和 MVVMLight介绍 MVVM是Model-View-ViewModel的简写.类似于目前比较流行的MVC.MVP设计模式,主要目的是为了分离视图(View)和模型(Model)的耦合. 它是一种极度优秀的设计模式,但并非框架级别的东西,由MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构. 立足于原有MVP框架并且把WPF的新特性糅合进去,以应对PC端开发日益复杂的需求变化. 结构如图所示: 相对于之前把逻辑结构写在Co

  • npm脚本库组织在项目中的地位详解

    目录 一.脚本的地位 二.“脚本调度”的难题 三.如此简单? 四.此剑名曰: npm-run-all 4.1 安装 4.2 第一个命令: npm-run-all 4.3 第二个命令:npm-s 4.4 第三个命令:npm-p 4.5 通配符 4.6 更多实用能力 一.脚本的地位 脚本是 项目真正的入口 . 无论你是刚刚 clone 完公司的项目,抑或是你准备在开源社区做一点微小的贡献:你需要做的第一件事,永远是: 打开 package.json,看看 scripts 里都有哪些脚本. 有些脚本负

  • vue项目中axios使用详解

    axios在项目中(vue)的使用 没有vue项目的使用vue-cli脚手架生成一个webpack模板的项目即可愉快的看下去了~ 如果开发遇到跨域问题可以参考:http://www.jb51.net/article/134571.htm 安装axios到项目中 npm install axios --save 配置wepack别名,不同环境访问不同的配置接口 配置: 使用:import config from 'config' 封装一个axios实例 新建fetch.js,在此创建axios实例

  • 如何在ASP.NET Core类库项目中读取配置文件详解

    前言 最近有朋友问如何在.net core类库中读取配置文件,当时一下蒙了,这个提的多好,我居然不知道,于是这两天了解了相关内容才有此篇文章的出现,正常来讲我们在应用程序目录下有个appsettings.json文件对于相关配置都会放在这个json文件中,但是要是我建立一个类库项目,对于一些配置比如密钥或者其他需要硬编码的数据放在JSON文件中,在.net core之前配置文件为web.config并且有相关的类来读取节点上的数据,现如今在.net core中为json文件,那么我们该如何做?本

  • HttpClient 在Java项目中的使用详解

    Http协议的重要性相信不用我多说了,HttpClient相比传统JDK自带的URLConnection,增加了易用性和灵活性(具体区别,日后我们再讨论),它不仅是客户端发送Http请求变得容易,而且也方便了开发人员测试接口(基于Http协议的),即提高了开发的效率,也方便提高代码的健壮性.因此熟练掌握HttpClient是很重要的必修内容,掌握HttpClient后,相信对于Http协议的了解会更加深入. 一.简介 HttpClient是Apache Jakarta Common下的子项目,用

  • vue-cli项目中使用Mockjs详解

    背景 前端在早期jQuery时代时,前端功能和后端工程基本上都是合在一起,典型的就是常见的maven工程下面的webapp目录包含前端各类静态资源文件. 这个时候,我们总是会遇到这些问题: 老大,接口文档还没输出,我的好多活干不下去啊! 后端小哥,接口写好了没,我要测试啊! 测试时间不够啊,就要发版了,今天难道我有看明天的太阳升起? 诸如种种,就是一句话:劳资,再也不要指望你们了! node出现之后,准确的说是前后端分离之后,前端迫切需要一种机制,不在需要依赖后端接口开发.经过这几年的发展,有好

  • Java Web Fragment在项目中使用方法详解

    Web Fragment 是什么 - 它是在 servlet 3.0开始支持的,可以把一个dy web项目拆分为多个项目,解耦合,使其在项目中开发效率提高,下面我演示简单的项目创建过程 用eclipse右键new->other->web->web fragment project 项目结构 web-fragment.xml 配置详细内容 <?xml version="1.0" encoding="UTF-8"?> <web-fra

  • Spring MVC项目中的异常处理详解

    目录 前言 1. 基于配置的简单异常处理 2. 基于注解的全局异常处理 总结 前言 我们在项目的开发中,难免会遇到各种可预知的.不可预知的异常需要处理.每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大. 那么,能不能将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的 功能较单一,也实现了异常信息的统一处理和维护?答案是肯定的.下面将介绍Spring MVC是如何处理异常的. 1. 基于配置的简单异常处理 在SpringMVC中拥有一套非常强大的

随机推荐