要说c++或者c中最容易滥用的特性,莫不过于宏。

事实上,宏有很多缺点,比如难以调试,容易造成污染。(比如Windows.h里就定义了max和min宏,毫无避讳。如果调用xxx.max()之类的函数,那么恭喜你的max被替换了)

所幸,C++为我们提供了一套机制来减少宏的使用。

使用std::source_location来替代__LINE__和__FILE__等

不仅如此,std::source_location还能显示列号等宏并不实现的功能。

std::source_location的用法如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <source_location>
#include <iostream>

void test(const std::source_location& location = std::source_location::current()){
    std::cout << "at " << local.file_name() << ":" <<local.line() << std::endl;
}

int main(){
    test()
}

至于std::source_location是怎么实现获取源代码位置的呢,可以看看实现。

现在看看这个std::source_location::current()的实现,其实并不高深:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
_NODISCARD static consteval source_location current(const uint_least32_t _Line_ = __builtin_LINE(),
        const uint_least32_t _Column_ = __builtin_COLUMN(), const char* const _File_ = __builtin_FILE(),
        const char* const _Function_ = __builtin_FUNCTION()) noexcept {
        source_location _Result;
        _Result._Line     = _Line_;
        _Result._Column   = _Column_;
        _Result._File     = _File_;
        _Result._Function = _Function_;
        return _Result;
    }

小小的提示:__builtin_XXXX可以获取调用此函数的源代码处某些信息,内置于编译器中。

五秒钟的时间来思考他是怎么工作的。


时间到!

答案就是:C++会在调用函数之前构造参数,并在调用处构造,即使是默认参数也是如此。编译器会自动在函数的的调用处调用std::source_location::current()来获取源代码参数位置。

这样,我们就把代码中的__FILE____LINE__进行了替换。很轻松,不是么?

使用模板来替换一部分宏

要说把宏完全替换,这自然是不大可能的,但是用模板,就能替换绝大部分代码生成宏。

比如,笔者曾写过一段用于定义一个异常类的宏:

1
2
3
4
#define MAKE_EXCEPTION(Name)                            \
    class Name ## Excpetion : public Exception{    \
        /* more define ... */                           \
    }

仔细一看,这这种代码生成视乎是模板做不到的,但事实并非如此,而且可以做的更好:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// @brief 字符串字面值
    template<size_t N>
    struct StringLiteral {
        constexpr StringLiteral(const char (&str)[N]) {
            std::copy_n(str, N, value);
        }

        char value[N];
    };

// 我们要生成异常的主要模板
template<utopia::core::StringLiteral exception_name>
    class UniversalException : public Exception {
      public:
        inline virtual std::string get_name() const noexcept override {
            return std::string{ exception_name.value };
        }
    };

// 用法如
using IOException = UniversalException<"IOException">;
// throw IOException{"I'm a exception."};

// 我们就定义出了一个新异常!
// std::is_same_v<IOException,UniversalException<"IOException">> == true
// std::is_same_v<IOException,UniversalException<"OtherException">> == false

// 并且我们支持比宏更多的用法!
// --------------------------------↓ 通过继承来使用
class FileSystemException : public UniversalException<"IOException">{
    // ...
}

使用inline等替换宏

宏最饱受诟病之处就在于,宏难以调试,难以知道复杂宏的展开结果。

但是使用inline就可以等价进行替代。并且inline的控制比宏更加灵活,可以做到:Debug时不展开函数,最大化地方便Debug。Release时完全展开,和宏的性能无异。

可见:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 如MD5算法的实现
//F,G,H,I四个非线性变换函数
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))


// 使用inline和模板可以定义作:
namespace MD5{

    template<typename T>
    inline T F(T x,T y,T z){
        return (((x) & (y)) | ((~x) & (z)));
    }
    template<typename T>
    inline T G(T x,T y,T z){
        return (((x) & (z)) | ((y) & (~z)));
    }
    template<typename T>
    inline T H(T x,T y,T z){
        return ((x) ^ (y) ^ (z));
    }
    template<typename T>
    inline T I(T x,T y,T z){
        return ((y) ^ ((x) | (~z)));
    }

}
// 用法上几乎无异,但调试难度降低了一个级别
// 并且我们成功防止了命名空间污染
// 想一想全世界的程序员都把宏的名字定义成 F G H I 或者类似的名字会怎么样?
// Windows API提供的windows.h已经把max min定义成了宏,引起了许多问题

防止宏污染

c++20添加了module机制。

视乎并没有什么值得注意的,但有一个小小的highlight:通过import关键字导入一个模块不会引入这个模块定义的宏。

听起来不错,是吧?

但截至目前,各大构建系统对c++20 module的支持仍然不乐观。

只能依靠程序员多做一点,来让这个世界变得更美好。

DONE.