关于宏方法的理解
宏方法和
- 批量定义方法,和模板比较
- 校验,失败并返回
- 宏常量,和const常量比较
一、伪函数方法和模板方法
以下是open62541三方库的opcua server中的一段代码
#define UA_ENCODING_HELPERS(TYPE,UPCASE_TYPE)\static UA_INLINE size_t\UA_##TYPE##_calSizeBinary(const UA_##TYPE *src){\return UA_calSizeBinary(src, &UA_TYPES[UA_TYPES_##UPCASE_TYPE])\}\static UA_INLINE UA_StatusCode\UA_##TYPE##_encodeBinary(const UA_##TYPE *src, UA_Byte **bufpPos, const UA_Byte * bufEnd){\return UA_encodeBinaryInternal(src, &UA_TYPES[UA_TYPES_##UPCASE_TYPE],\bufPos,&bufEnd,NULL,NULL)\}\static UA_INLINE UA_StatusCode\UA_##TYPE##_decodeBinary(const UA_##TYPE *src, size_t *offset, UA_##TYPE *dst){\return UA_decodeBinaryInternal(src, offset,dst,\&UA_TYPES[UA_TYPES_##UPCASE_TYPE])\}UA_ENCODING_HELPERS(Boolean,BOOLEAN)
UA_ENCODING_HELPERS(SByte,SBYTE)
这个UA_ENCODING_HELPERS宏可以为boolean类型、SByte类型批量申明定义三个函数。
以boolean为例,生成了UA_Boolean_calSizeBinary、UA_Boolean_encodeBinary、UA_Boolean_DecodeBinary三个方法
以SByte为例,生成了UA_SByte_calSizeBinary、UA_SByte_encodeBinary、UA_SByten_DecodeBinary三个方法
这样做的好处是,
- 可以为每个类型生成独属于自己计算二进制大小、加密、解密方法,
- 另外可以减少代码量。
- 调用处增加可读性
关于减少代码量,这里申明定义了两个类型的各自的三个方法,一共6个方法,宏方法14行,之后每增加一个类型的,只需要多谢一行代码,提高开发速度。如果不是使用这样的方法,需要写的代码量为类型数(n)*宏方法代码行数(m)。但是需要注意的点是,减少的只是开发者需要写的代码量,而对于编译器而言,代码量并没有减少,因为在预编译期,使用宏的地方会被替换。
这样使用是因为UA_calSizeBinary、UA_encodeBinaryInternal、UA_decodeBinaryInternal这三个内层调用的函数,并没有入参类型上的区别。如果在三个内层调用函数中,对于不同类型,也就是UA_TYPES有不同处理方式,那么这样使用确实很方便。但是这三个内层函数并没有根据UA_TYPES的类型不同做不同处理,就显得有点鸡肋。如果直接使用三个内层函数也可以做到一样的效果。所以这里这样使用只是增加了在调用这三个宏定义方法时的可读性。
和模板方法的不同
-
宏在预处理器中被展开,模板方法在编译时被生成
-
模板方法需要后去模板参数才能生成对应代码
-
模板方法灵活性更好,对于需要特殊处理的模板参数,可以特例化模板,而宏需要使用if-else这类的手段
二、频繁校验时使用
在频繁需要进行校验,并且校验失败时直接退出函数时使用。
比如这样的场景,在使用proto数据时,有一个map<string,variant>,需要根据key(stirng)找到value(variant),然后需要校验variant是否是我们需要的类型比如stringvalue、boolvalue、int32value等,如果不等就退出函数,以及获取map中这个variant实际类型值
#define CHECK_RETURN(map,key,type,errorcode,getter,out)\auto iter_##type = map.find(key);\if(iter_##type == map.end()) return errorcode;\if(iter_##type->second.oneof_case() != type) return errorcode;\out = iter_##type->second.getter;\int32 startOpt(){...int32 id = 0;CHECK_RETURN(map,"id",int32,-10001,int32value(),id);string address;CHECK_RETURN(map,"address",string,-10002,stringvalue(),idaddress)return 0;
}
这样写在CHECK_RETURN中校验失败了,会直接返回错误码,结束startOpt函数,如果成功,会重新给id和address赋值。
如果是使用方法来校验
int32 checkAndReturn(mapType map,string key,Type type,Out& out)
{auto iter = map.find(key);if(iter == map.end()) return -10000;if(iter->second.oneof_case() != type) return -10000;out = iter->second.getter;return 0;
}
当在startOpt中用这个checkAndReturn替代CHECK_RETURN后,需要在startOpt再判断一次
int32 startOpt(){...int32 id = 0;auto ret = checkAndReturn(map,"id",int32,id);if(ret != 0) return -10001;string address;ret = checkAndReturn(map,"address",string,idaddress)if(ret != 0) return -10002;return 0;
}
这样会导致可读性变差,代码量增加,更加冗余。
三、宏变量
什么时候使用宏常量和什么时候使用const常量。个人认为使用宏常量的地方都可以替换为const常量,而且使用const常量更好。
以
#define PI 3.14159
和
const float PI = 3.14159
为例
以下是一些区别:
- 编译时处理:
#define PI 3.14159
是一个预处理指令,在编译前由预处理器进行处理。预处理器会将所有出现的PI
字符串替换为3.14159
字面量。const float PI = 3.14159
是一个常量声明,会在编译时被编译器处理。编译器会为这个常量分配内存空间,并在链接时确定它的值。
- 类型检查:
- 使用
#define
定义的PI
是一个宏,没有任何类型信息。预处理器只是简单地进行文本替换。 - 使用
const float PI = 3.14159
定义的PI
是一个类型为float
的常量。编译器可以进行类型检查,确保它在使用时类型正确。
- 使用
- 作用域:
- 使用
#define
定义的PI
在预处理阶段就被替换了,它的作用域仅限于定义它的那个编译单元。 - 使用
const float PI = 3.14159
定义的PI
是一个全局变量,它的作用域取决于它被声明的位置。
- 使用
- 调试信息:
- 使用
#define
定义的PI
在编译后的二进制文件中不再存在,因此很难在调试过程中查看它的值。 - 使用
const float PI = 3.14159
定义的PI
会保留在编译后的二进制文件中,可以在调试时查看它的值。
- 使用
另外还有一个重要的地方——编译解耦。
一般使用宏常量,会直接把宏写在.h文件中,我一般使用const会定义一个.h和.cpp文件。
//errorcode.cpp
const int64 NO_ID = -10001;
const int64 NO_NAME = -10002;//errorcode.h
extern const int64 NO_ID;
extern const int64 NO_NAME;
这样写会显得代码冗余,过多。但是如果这样写,不要errorcode.cpp文件,申明和定义都在errorcode.h文件中
//errorcode.h
const int64 NO_ID = -10001;
const int64 NO_NAME = -10002;
如果这个errorcode.h头文件被多个文件include,那么每次修改errorcode后(不增加和删除errorcode),都会在编译项目的时候,非常耗时。
而如果将其分开,每次errorcode.cpp文件的修改后再编译项目,就会编译很快