【问题标题】:Over reliance on macros过度依赖宏
【发布时间】:2023-04-10 16:41:01
【问题描述】:

我觉得,每次我阅读 C 或 C++ 程序时,其中一半或更多只是宏。我知道宏可能很酷,但它们很难跟踪、调试等。更不用说大多数编程语言甚至没有定义宏之类的东西(尽管 Perl6 会有类似的东西)。

我个人总是找到一种不使用宏来编写代码的方法,无论是使用模板、多重继承等。我什至觉得我不是一个好的程序员,因为所有专业人士都使用宏,我尽量避免尽我所能。

问题是,有没有没有宏无法解决的问题?宏最终是好还是坏的做法?我什么时候应该考虑使用宏?

【问题讨论】:

  • 好吧,你不能在 C 中使用模板,所以这是一个原因。
  • 如果您觉得“所有专业人士都使用宏”,您阅读了哪些代码?我所知道的所有理智的 C++ 代码都非常努力地避免它们。在 C 中,它有点不同,因为你没有很多选择。顺便说一句,为什么这会被标记为 C?您的问题听起来非常具体到 C++,提到了模板和继承。你知道,C 和 C++ 是不同的语言。
  • 正如@jalf 所说的那样——它们可能有一些相似之处,但在代码生成方面,它们完全不同。例如,X-macro/table generation idiom
  • 是的,很抱歉混用了它们。但是我不得不说,在这两种语言中,我都看到两种语言的代码都非常依赖宏,以至于无法阅读或遵循代码。
  • 大量自称为 C++ 程序员的人实际上只是在类框架中编写 C。他们不知道 C++ 有一个独特的设计习惯,所以他们的代码充满了数组、printf 和宏。顺便说一句,他们通常将两种语言称为共轭:“C/C++”。

标签: c++ c macros


【解决方案1】:

是的,这是一个。当您需要以一种配置包含跟踪代码而另一种配置完全省略的方式向程序添加跟踪代码时,您必须使用宏。

类似:

#ifdef WITH_LOGGING
    #define LOG( x ) DoLog( x )
#else
    #define LOG( x )
#endif

现在你这样使用它:

LOG( L"Calling blahblahblah with " + getSomeStringHardToCompute() );

在使用WITH_LOGGING 的配置中,您拥有该代码,否则它被完全省略 - 甚至不存在于二进制文件中,因此

  • 它无助于他人分析您的程序
  • 你会得到一个更小的二进制文件
  • 该程序根本不会浪费时间进行日志记录
  • 编译器可以生成更好的优化代码。

【讨论】:

  • 那是模板或内联函数。不需要宏。
  • @thomasrutter:是的,你可以,但这太可怕了——你会严重污染有用的代码。
  • @Basilevs:函数需要参数计算。在上面的示例中,如果使用函数调用,无论如何都会调用getSomeStringHardToCompute()。宏是消除这种情况的唯一方法。
  • @Basilevs:你错了。 除非编译器可以绝对确定这样做不会产生任何副作用,必须对参数进行评估。使它成为宏的另一个原因是它允许您自动从调用站点插入行号
  • @Basilevs:编译器知道它编译的每种类型的一切。为什么这是相关的?如果评估参数有副作用,则无法对其进行优化,否则编译器会改变程序的语义。
【解决方案2】:

您一直在查看一些糟糕的 C++ 代码。我使用宏的地方仅限于:

  • 头球后卫
  • 非常偶然的条件编译
  • 一般的异常抛出宏
  • 通用调试/记录输出宏

我认为这四个是无法避免的。

【讨论】:

  • 特别是因为记录/抛出您通常希望自动提取文件名和行号。
【解决方案3】:

直接来自 Scott Myer 的 Effective C++ -> 1

鉴于 const 和内联的可用性,您对预处理器的需求减少了,但并未完全消除。放弃#include 的日子还很遥远,而#ifdef/#ifndef 继续在控制编译方面发挥重要作用。现在还不是退役预处理器的时候,但您绝对应该计划开始给它更长、更频繁的假期。

【讨论】:

    【解决方案4】:

    调试行为可以通过常量标志或调试函数来控制。所以这是我的不可避免的清单:

    • 多重包含保护。
    • 宏是符号字符串化的唯一方式。 assert宏,const string & stringify(enum category value)的紧凑实现;

    例子:

    const char* stringify(enum category value)
    {
        #define c(x) case x: return #x;
        switch(value) {
            c(CIRCLE)
            c(RECTANGLE)
            c(TRIANGLE)
            default: return "UNKNOWN";
        }
        #undef c // the most important part
    }
    

    【讨论】:

      【解决方案5】:

      当然,当您想在预处理期间生成代码时,宏也很有用。虽然使用模板可以避免这种情况(请参阅此 SO 问题和讨论 - Are C++ Templates just Macros in disguise?),但如果它使用户的生活更轻松,您可以使用宏 - 请参阅“googletest”项目(https://github.com/google/googletest/)如何有效地使用宏。您显然不想使用宏来生成需要调试的代码,而是使用模板。

      【讨论】:

        【解决方案6】:

        我认为 C++ 的模板和内联函数使宏几乎可以避免。

        宏的普及可能是因为有很多 C++ 程序员曾经是 C 程序员。这些人可能会精通使用宏(因为它有时确实是纯 C 中最好或唯一的解决方案),如果他们已经知道如何解决问题,他们可能看不到学习更复杂的 C++ 特性的任何意义。至少在开源世界里,有很多 C 转换者,所以你自然会遇到 C 范式。如果你避免这样的功能,我不认为你是一个糟糕的程序员,很多人都会这样做,就像 GOTO 一样。

        C(因此也是 C++)是一种极其灵活的编程语言。这很好,因为每个人都可以发展出自己独特的风格,并以几种不同的方式解决大多数问题。然而,这也可以被认为是一个问题。在我看来,这不是应该通过语言来解决的问题,而是通过建立约定来解决的。

        C++ 中有许多特性可以安全地忽略。也许有些奇怪的特殊场合,这样的功能确实是最好的方法,但在大多数情况下,你可以不用:

        • 朋友班
        • 转到 还有更多。

        IMO,一名高级 C++ 程序员至少应该能够流利地阅读它们 - 但我希望优秀的程序员仔细考虑何时以及是否使用臭名昭著的功能。

        【讨论】:

          【解决方案7】:

          没有宏我无法解决很多问题。 比如一些结构体的序列化/反序列化

          #define STRUCT_DESCRIPTION structname(MyStruct) member(int,a) member(double,b) member(long, c) #include "declare_serializable_struct.h" // 声明结构本身并生成序列化/反序列化代码 #undef STRUCT_DESCRIPTION

          (也可以使用 BOOST_PP_SEQUENCE) 另一个例子 - 使用消息映射调度消息,即生成这样的开关:

          开关(消息类型) { 案例 msg1: on_msg1(msg);休息; 案例 msg2: on_msg2(msg);休息; ... }

          同时使用一些消息描述表(“map”)生成处理方法声明on_msgX(msg)

          就个人而言,我尽可能避免使用宏,但我没有以这种方式成功。

          但是,c++0x 中的 lambdas 允许将任意代码内联到“用户或库定义的语言语句”中,例如 foreach 循环,因此宏领域失去了重要部分:)

          【讨论】:

            【解决方案8】:

            宏是条件编译的解决方案(ifdefifndef)。下面是例子:

            1)

            #ifndef MY_HEADER_HPP
            #define MY_HEADER_HPP
            
            //...
            
            #endif
            

            2)

            #ifdef __cplusplus
            #define BEGIN extern "C" {
            #define END }
            #define NULL (0);
            #else
            #define BEGIN
            #define END
            #define NULL ((void*)0);
            #endif
            
            //-------------------------
            
            BEGIN
            
            void my_function(char* str);
            
            END
            
            //-------------------------
            
            void my_function(char* str)
            {
                if(str != NULL)
                {
                    //...
                }
            }
            

            但是内联函数模板替代了C++中宏的其他用法。

            【讨论】:

              【解决方案9】:

              我倾向于尽可能避免使用宏,因为它们存在明显的安全/调试问题,但是有时宏提供的东西是该语言中没有其他工具可以优雅地做到的,在这种情况下,我更喜欢使用宏只是因为它让我(以及我的其他开发人员)的生活更轻松。

              例如,我创建了一个Enum 类,它在struct(作用域)中包装了一个枚举并添加了一些功能:

              • 迭代的可能性(这意味着值的顺序)
              • 从字符串转换为/从字符串(方便读取/写入文件,写入日志)

              为了创建枚举,我使用了一个宏,它会自动生成转换器(往返)和迭代向量。

              当然我可以不用它,毕竟宏只是用于代码生成。但是没有一个就意味着违反 DRY,在我自己的小偏好中“DRY”>“不要使用宏”。因为一旦调试宏是安全的,而违反 DRY 是维护的噩梦。

              现在,一旦我发现如何不违反 DRY,我就完全赞成放弃这个宏。想法显然是受欢迎的......而且外部脚本不是更好;)

              我的 2 美分。

              【讨论】:

                【解决方案10】:

                我也尽量避免使用宏,但是为了扩展调试,我还没有找到调试时打印文件名、函数名和行号的方法。

                我通常有一个名为 DebugLog.h 的头文件,其中包含以下宏

                #define DEBUG(debugMessage) \
                   printf("%s | %s [%d] - %s\n", __FILE__, __PRETTY_FUNCTION___, debugMessage);
                

                使用: 调试(“测试”) 将输出如下内容:

                main.cpp | foo(void)[20] - Test
                

                您可以为 C++ 和其他调试语句调整宏。也可以修改宏以将结果字符串发送到记录器。

                【讨论】:

                  【解决方案11】:

                  我开始在一家电信公司工作。产品代码库大约有 20 年的历史,必须支持许多遗留产品,同时还要尽量避免重复代码。使用的语言是 C++03。我发现很多类似于以下的构造

                  ClassA::methodA(...)
                  {
                     // Common code
                     ...
                  
                  #if defined(PRODUCT_A) || defined(PRODUCT_B)
                     // Code for Product A or Product B
                     ...
                  #elif defined(PRODUCT_C)
                     // Code for product C
                     ...
                  #endif
                  
                    // Common code
                    ...
                  }
                  

                  可怕的东西,我同意。到目前为止,我们还没有找到更好的解决方案。至少通过这种方法,我们可以通过简单的代码阅读来理解代码应该做什么。

                  【讨论】:

                    【解决方案12】:

                    问题是,有没有没有宏解决不了的问题?

                    没有。

                    宏最终是一种好的/倒退的​​做法吗?什么时候应该考虑使用宏?

                    在不支持或不尊重 inline 关键字的语言中,宏是重用代码的好方法,但同时避免了在任何紧密循环的代码中的函数调用开销这会产生巨大的影响。

                    您对代码中充斥着宏的咆哮可能是有道理的。确实很难调试,在某些情况下很难阅读。但它们确实在极少数情况下很有用,这样的优化是真正有必要的。

                    请注意,从 C99 开始,C 现在可以使用 inline 关键字执行显式内联函数,这减少了对宏甚至 has advantages over using macros 的需求。

                    【讨论】:

                    • 由于很挑剔,您需要宏来包含保护和条件编译,并且在许多框架中,如果禁用日志记录,则可以避免日志记录成本。
                    • 同意大卫。有些问题只能用宏优雅地解决(优雅地是一个相对术语)。
                    • 不,因为有和没有宏的 C++ 图灵完备? :) 但是在很多情况下,只有宏(不是模板或内联)允许避免代码重复。我同意尽可能避免使用宏应该是一种意图,但诚实的答案必须是“是”。
                    • @thomasrutter:如果你可以在没有宏的情况下做任何事情,请告诉我如何编写一个函数来记录来自调用站点的文件名/行号,以及自定义日志消息。使用宏很简单。没有是不可能的。
                    • @jalf:log(__FILE__, __LINE__) << "Custom Message: " << i; 不工作吗?当然丑了……
                    【解决方案13】:

                    编程语言宏有利于所有宏的优点:避免一遍又一遍地输入相同的内容。所以如果你发现自己在很多地方都写了相同的代码,为什么不把它做成一个宏呢?特别是如果您正在编写一个库,使用宏可以使尝试使用该库的人的生活更轻松。看看几乎所有的 GUI 工具包(Qt 就是一个例子)。他们都大量使用宏。

                    【讨论】:

                    • 这就是我们使用函数的原因,而不是使用宏的原因。
                    • 这不是宏的好理由,有更优雅的解决方案更安全地解决相同的问题(函数)
                    • 当一个函数无法做到这一点时,你应该使用预处理器宏
                    猜你喜欢
                    • 1970-01-01
                    • 2014-04-03
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-10-29
                    • 1970-01-01
                    • 2020-08-12
                    相关资源
                    最近更新 更多