Macro Free in C++

Macro Free In Cpp

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

背景

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

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

如何替代宏的使用

  1. 表达式别名

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define PI 3.14
#define SEVEN 3 + 4
#define FILENAME "header.h"
#define SUM a + b
void 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();
}
  1. 类型别名

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

1
2
3
#define A T
// ========================>>>
using A = T;
  1. 参数表达式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
#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++的特性。完美转发使得调用方可以根据需要决定参数传递的类型。

  1. 参数化类型别名

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

1
2
3
4
#define AliasMap(T) std::map<std::string, T>;
// ========================>>>
template <typename T>
using AliasMap = std::map<std::string, T>;
  1. 条件编译

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

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

1
2
3
4
5
6
7
8
void draw_triangle()
{
#if _WIN32
// Windows triangle drawing code here
#else
// Linux triangle drawing code here
#endif
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void do_sth()
{
#if DEBUG_MODE
log();
#endif
// …
}
// ========================>>>
void do_sth()
{
if constexpr (DEBUG_MODE) {
log();
}
#endif
// …
}

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

当然if constexpr的使用也是有其不足之处的,以上面的draw_triangle函数为例,即便某个条件分支不会被使用,你仍然需要有相关冗余的声明。所以对于这种情况,个人建议还是不需要使用if constexpr替代宏。

  1. 源码位置

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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++写法来替换不够安全的、使用了宏定义的老式代码,事实上C++的发展过程中一直在提出一些减少预处理宏使用依赖的方案。但从目前来看,还是有不少预处理使用无法替换,即便如此,个人认为适当使用宏和合适的,其AST的生成功能是非常强大的工具,并且某种情况下能使得代码更加易读。

参考资料

  1. 《cppcon 2019》——https://www.youtube.com/watch?v=c6NkeF1eChs
  2. 《Rejuvenating C++ Programs through Demacrofication》——https://www.stroustrup.com/icsm-2012-demacro.pdf
  3. 《if statement》——https://en.cppreference.com/w/cpp/language/if
  4. 《The year is 2017 - Is the preprocessor still needed in C++?》——https://foonathan.net/2017/05/preprocessor/