暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C++系列学习——Macro Free

码著 2021-04-04
220

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++表达式,对于这种情况比较简单的是使用常量表达式或者lambda替换宏,

    #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();
    }
    复制
    • 类型别名

    类型别名是一个类似于对象的宏,其替换文本可以识别为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++写法来替换不够安全的、使用了宏定义的老式代码,事实上C++的发展过程中一直在提出一些减少预处理宏使用依赖的方案。但从目前来看,还是有不少预处理使用无法替换,即便如此,个人认为适当使用宏和合适的,其AST的生成功能是非常强大的工具,并且某种情况下能使得代码更加易读。


                参考资料

                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++》


                码著

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


                文章转载自码著,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论