C++系列学习——Macro Free

Posted 码著

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++系列学习——Macro Free相关的知识,希望对你有一定的参考价值。

C++系列学习--Macro Free



One of C++'s aims is to make C's preprocessor redundant because I consider its actions inherently error prone.

——Bjarne Stroustrup

背景

C预处理器本质是一个文本替换工具,用来在实际编译之前进行一定的预处理操作,一般情况下#开头的预处理操作并不认为是语言本身的一部分,因为编译器永远看不到这些宏定义符号。

以C++来说,用宏的目的并不是出于性能的缘由,更多的只是为了减少重复的代码和进行条件编译。随着modern cpp的发展,越来越的新特性加入使得对宏的使用依赖进一步降低。本文将关注如何使用C++新特性替换C预处理程序。

C++系列学习——Macro Free

如何替代宏的使用

  • 表达式别名

有一些宏定义会用在表达式别名,替换后的文本会被识别为C++表达式,对于这种情况比较简单的是使用常量表达式或者lambda替换宏,

#define PI 3.14#define SEVEN 3 + 4#define FILENAME "header.h"#define SUM a + bvoid summer(){ int a = 1, b=2; int c = SUM;}// ========================>>>constexpr auto PI = 3.14;constexpr auto SEVEN = 3 + 4;constexpr auto FILENAME = "header.h";void summer(){ int a = 1, b=2; auto SUM = [&a, &b]() { return a + b; }; int c = SUM();}
  • 类型别名

类型别名是一个类似于对象的宏,其替换文本可以识别为C++类型表达式。对于这种,可以使用C++的别名声明来替换:

#define A T// ========================>>>using A = T;
  • 参数表达式

参数表达式是一种类似于函数的宏,替换文本后会扩展为表达式或语句。对于这种使用,C++中的最佳实践是使用内联模版函数。

#define MIN(A, B) ((A) < (B) ? (A) : (B))#define ASSIGN(A, B) { B = A; }// ========================>>>template <typename T1, typename T2>inline auto MIN(T1&& A, T2&& B) -> decltype(((A) < (B) ? (A) : (B))){ return ((A) < (B) ? (A) : (B));}template <typename T1, typename T2>inline void ASSIGN(T1&& A, T2&& B) { B = A;}

这里引入了内联,自动的推导类型和完美转发等modern c++的特性。完美转发使得调用方可以根据需要决定参数传递的类型。

  • 参数化类型别名

这种其实就是模版别名,在C++11之前需要用宏去实现。

#define AliasMap(T) std::map<std::string, T>;// ========================>>>template <typename T>using AliasMap = std::map<std::string, T>;
  • 条件编译

目前绝大多数开源的C++项目都会依赖宏来进行条件编译,其本质意义是通过定义宏与否来改变某个定义/声明。

比如存在一个绘制三角形的API,但其具体实现会根据操作系统而变化,通过预处理器就可以很好地实现类似的兼容:

void draw_triangle(){ #if _WIN32 // Windows triangle drawing code here  #else // Linux triangle drawing code here #endif}

其中某个分支的代码会在进行编译之前被去掉,这样编译时就不会出现API未定义的错误。

在C++17中有了新的语法特性if constexpr,我们可以用来替代一部分#if … #else的使用。以下面的使用为例:

void do_sth(){ #if DEBUG_MODE log(); #endif // …}// ========================>>>void do_sth(){ if constexpr (DEBUG_MODE) { log();  } #endif // …}

使用if constexpr的好处是其只会检查语法错误,像宏那样的使用方式,一旦DEBUG_MODE出现typo的错误,编译器是无法准确辨识的。

  • 源码位置

目前几乎所有的断言或者宏会用到宏,比如需要使用__LINE__, __FILE__, __func__ 等定位断言的位置,又或者需要断言开关等等。

要想替代对这些宏的使用则需要用上C++20的std::source_location,该类可以表示关于源码的具体信息,例如文件名、行号以及函数名。

#include <iostream>#include <string_view>#include <source_location> void log(std::string_view message, const std::source_location& location = std::source_location::current()){ std::cout << "info:" << __FILE__ << ':' << __LINE__ << ' ' << message << '\n'; // ========================>>> std::cout << "info:" << location.file_name() << ':' << location.line() << ' ' << message << '\n';}
C++系列学习——Macro Free

总结

这里提供了一些更“现代”的C++写法来替换不够安全的、使用了宏定义的老式代码,事实上C++的发展过程中一直在提出一些减少预处理宏使用依赖的方案。但从目前来看,还是有不少预处理使用无法替换,即便如此,个人认为适当使用宏和合适的,其AST的生成功能是非常强大的工具,并且某种情况下能使得代码更加易读。

C++系列学习——Macro Free

参考资料

1. 《cppcon 2019——Are We Macro-free Yet?》

2. 《Rejuvenating C++ Programs through Demacrofication》

3. 《The year is 2017 - Is the preprocessor still needed in C++》


码著

学点有意思的东西.....


以上是关于C++系列学习——Macro Free的主要内容,如果未能解决你的问题,请参考以下文章

VS 2010 中的 C++ 调试访问冲突

具有 GCC 优化的 C++ 代码导致核心在字符串上具有无效的 free()

C++ 解释器/控制台/片段编译器

Anchor free系列网络之YOLOX源码逐行讲解篇--解耦Head的搭建及分析

Anchor free系列网络之YOLOX源码逐行讲解篇--mosaic数据增强及数学理解

Anchor free系列网络之YOLOX源码逐行讲解篇--coco数据载入及分布式训练