【问题标题】:Is this Macro Abuse?这是宏滥用吗?
【发布时间】:2011-04-07 12:15:40
【问题描述】:

我正在对一些代码进行逆向工程并遇到了这个......

/************************************************************************/
/*                                                                      */
/*                          MACRO CHECK_FREAD                           */
/*                                                                      */
/*  CHECK_FREAD is used to check the status of a file read.  It         */
/*   is passed the return code from read and a string to print out if   */
/*   an error is detected.  If an error is found, an error message is   */
/*   printed out and the program terminates.  This was made into a      */
/*   macro because it had to be done over and over and over . . .       */
/*                                                                      */
/************************************************************************/

#define CHECK_FREAD(X, msg)  if (X==-1) \
                 { \
                return(DCD_BADREAD); \
                 }

基本上,此文件中的特定函数集在执行 (c-read) 以检查结果是否有错误时调用此函数。他们也对 EOF 进行了类似的检查...基本上据我所知,他们这样做是为了在函数中间的很多地方插入错误返回。

例如

/*  Read in an 4                */
    ret_val = read(fd, &input_integer, sizeof(int32));

    CHECK_FREAD(ret_val, "reading an 4");
    CHECK_FEOF(ret_val, "reading an 4");

    if (input_integer != 4)
    {
        return(DCD_BADFORMAT);
    }

    /*  Read in the number of atoms         */
    ret_val = read(fd, &input_integer, sizeof(int32));
    *N = input_integer;

    CHECK_FREAD(ret_val, "reading number of atoms");
    CHECK_FEOF(ret_val, "reading number of atoms");

现在我才刚刚回到 c/c++ 编程领域,而且我一开始就很少使用宏,但是这种宏是滥用吗?

我读到了... When are C++ macros beneficial?

...听起来不像任何犹太洁食的例子,所以我的猜测是“是”。但我想获得更明智的意见,而不仅仅是做出有根据的猜测...... :)

...errr,使用某种包装不是更好吗?

【问题讨论】:

  • 此宏不会按照 cmets 中的描述打印错误消息。这是完整的代码吗?
  • 是的,这是完整的代码。对于 ChrisF,请参阅我添加的示例。
  • 为什么不使用'msg'参数?注释与实现不匹配,这意味着至少有一个是错误的,但可能两者都有。最好编写一个size_t checked_fread(void *buffer, size_t itemsize, size_t numitems, FILE *fp, const char *msg) 函数并始终如一地使用它。推测它为什么检查 -1 也很有趣; fread() 返回 0 或错误计数 - 返回类型为 size_t,几乎所有目的都排除了 -1 的返回值。
  • 但这算不算滥用?那是我的问题。 @Jonathan我不知道他们为什么不使用味精......健忘?懒惰?困惑?你选!但你说得对,这很奇怪....
  • 他们可能在测试代码时使用了 msg 属性,并在发布之前拿走了打印代码。我最初认为它是一种在代码中插入 cmets 的方法,但为什么不改用诚实的 /* cmets */ 呢?

标签: c file-io macros coding-style


【解决方案1】:

因为不一样。如果你把同样的东西放到一个函数中,你会得到一个短函数,如果X 等于-1,则返回DCD_BADWRITE。然而,有问题的宏从 包含 函数返回。示例:

int foobar(int value) {
    CHECK_FREAD(value, "Whatever");
    return 0;
}

将扩展为:

int foobar(int value) {
    if (value == -1) {
        return (DCD_BADWRITE);
    };
    return 0;
}

但是,如果它是一个函数,那么外部函数 (foobar) 会很高兴地忽略结果并返回 0。

一个看起来很像函数,但在关键方面表现不同的宏是一个主要的禁忌 IMO。我会说是的,它宏滥用。

【讨论】:

  • 如果他们正在读取大量值并希望消除重复代码,还有什么选择?有没有一种很好的方法来做他们正在做的事情(请参阅我编辑的帖子以获取示例案例)?关于我能想到的唯一一件事,他们应该将阅读过程分成不同的部分,只要文件有不同的部分......
  • 在 C++ 中,异常。将代码放入一个小函数中,而不是滥用返回值来报告失败,而是抛出错误。然后在使用代码中,将整个东西放在try/catch 中,并从catch 返回 DCD_BADWRITE。如果它是纯 C,那么 setjmp - 样式错误处理可能是更好的解决方案。然后,也许假设的程序首先使用循环会更好。
  • @Jason:我只是认为想要消除这里的重复代码是不合法的。尤其是考虑到所有消息都是完全冗余的事实后,它们的“消除后”代码甚至不会比if (retval != -1) return DCD_BADREAD 短得多。它当然不会更容易编写或理解。
  • 说在 only 的情况下不使用宏是值得使用的(你需要文本替换,而不是函数)有点荒谬。
【解决方案2】:

这不能作为函数完成,因为return 是从调用函数返回的,而不是宏中的块。

这种东西给宏起了个坏名声,因为它隐藏了一个重要的控制逻辑。

【讨论】:

  • 在 C++ 中,应该使用“throw”来处理这类事情。在 C 中,合理命名的宏通常是最好的选择。有关更多信息,请参阅我的完整答案。
【解决方案3】:

我对是否调用这样的宏*abuse*犹豫不决,但我会说这不是一个好主意。

我认为一般来说,您应该避免在宏中使用return。很难创建一个正确处理返回的宏,而不会使调用它的函数的控制流变得尴尬或难以理解。您绝对不想在没有意识到的情况下“意外地”从函数返回。调试起来可能很烦人。

此外,此宏可能会导致代码流出现问题,具体取决于它的使用方式。比如代码

if (some_condition)
    CHECK_FREAD(val, "string")
else
    something_else();

不会像你想的那样做(else 与宏内的if 相关联)。宏需要包含在 { } 中,以确保它不会改变任何周围的条件。

另外,没有使用第二个参数。这里明显的问题是实现与文档不匹配。隐藏的问题是当这样调用宏时:

char* messages[4];          // Array of output messages
char* pmsg = &messages[0];
....
CHECK_FREAD(val, pmsg++);

您希望在调用宏后指针移动到下一个数组元素。但是,没有使用第二个参数,因此增量永远不会发生。这也是为什么带有副作用的语句会导致宏出现问题的另一个原因。

与其使用宏来完成这一切,不如编写一个函数来封装read 和错误检查?它可以在成功或错误代码时返回零。如果错误处理代码在所有 read 块之间是通用的,您可以执行以下操作:

int your_function(whatever) {
    int ret;
    ...

    if ((ret=read_helper(fd, &input_integer)) != 0)
        goto error_case;

    ...

    // Normal return from the function
    return 0;

error_case:
    print_error_message_for_code(ret);
    return ret;
}

只要您对read_helper 的所有调用都将它们的返回值分配给ret,您就可以在整个函数中重复使用相同的错误处理代码块。您的函数print_error_message_for_code 将简单地将错误代码作为输入,并使用switch 或数组打印出与其对应的错误消息。

我承认很多人害怕goto,但是重用常见的错误处理代码而不是一系列嵌套块和条件变量可能是一个合适的用例。只需保持简洁易读(每个函数一个标签,并保持错误处理代码简单)。

【讨论】:

    【解决方案4】:

    这是否是滥用是一个品味问题。但我发现它存在一些主要问题

    1. 文档是完整的 错了
    2. 实施非常危险 因为if 和一个可能的 悬空else问题
    3. 命名并不意味着 是一个初步的回报 周边功能

    所以这绝对是非常糟糕的风格。

    【讨论】:

      【解决方案5】:

      如果宏在源文件中并且仅在该文件中使用,那么我发现它比在某个地方的某个标题中关闭时更具攻击性。但我不太喜欢返回的宏(尤其是记录 以终止应用程序并实际返回的宏),更不喜欢有条件返回的宏,因为它很容易创建以下内存泄漏:

      char *buf = malloc(1000);
      if (buf == 0) return ENOMEM;
      
      ret_val = read(fd, buf, 1000);
      CHECK_READ(ret_val, "buffer read failed")
      
      // do something with buf
      
      free(buf);
      

      如果我相信文档,我没有理由怀疑此代码在读取失败时会泄漏内存。即使文档是正确的,我也更喜欢 C 中的控制流非常明显,这意味着不会被宏隐藏。我还希望我的代码在我有资源要清理的情况和我没有资源的情况之间看起来模糊一致,而不是对一个使用宏而不是对另一个使用宏。所以这个宏不符合我的口味,即使它不是绝对滥用。

      要按照文档所说的去做,我更喜欢这样写:

      check(retval != -1, "buffer read failed");
      

      check 是一个函数。或使用assert。当然,断言只有在未设置 NDEBUG 时才会执行任何操作,因此如果您计划单独的调试和发布版本,它对错误处理没有好处。

      要执行宏实际执行的操作,我宁愿完全跳过宏,并编写:

      if (retval == -1) return DCD_BADREAD;
      

      这几乎不是一长串代码,将它的不同实例共用起来并没有真正的好处。认为您将通过封装/隐藏现有的、有据可查的方法来改进事情也可能不是一个好主意,read 通过这些方法指示错误,这是 C 程序员广泛理解的。如果不出意外,此检查完全忽略了 read 可以产生比您要求的数据少的事实,没有明显的原因。这不是宏的直接错误,但宏背后的整个想法似乎来自糟糕的错误检查。

      可能这里发生的情况是他们曾经有一个终止的宏,然后他们决定在发生故障时不想立即中止程序,而是希望返回此错误代码 DCD_BADREAD。他们应该做的是完全删除宏并更改所有调用站点 - 如果您更改函数的控制逻辑并更改它返回的内容,最好明显更改该函数的源。相反,他们所做的是做出尽可能少的改变,从而达到他们想要的结果。这在当时似乎是个好主意(遗憾的是,忘记更新 cmets 是一个常见错误)。但正如每个人似乎都同意的那样,它导致了非常狡猾的代码。

      控制流的宏可以是非滥用的,但我认为只有在它们看起来像控制流并且被准确记录的情况下。 Simon Tatham 的协程宏浮现在脑海中 - 有些人一开始就不喜欢协程,但假设你接受这个前提,一个名为 crReturn 的宏至少 看起来返回。

      【讨论】:

      • 最有可能的是,在调试期间,启用在 msg 中记录值并返回的代码很有用。在生产过程中,日志代码是不必要的。保留宏允许在必要时重新启用日志记录。
      • @supercat:所以文档描述了文件中不存在代码的调试行为,但调试程序员应该在需要时实现并插入?而不是存在哪些代码的生产行为?那么是的,这无疑是一种巨大的滥用;-)
      • 日志代码很可能在过去的某个时间点确实存在,但已被删除。我的偏好是为日志记录部分使用嵌套宏,并使用 LOGGING_ENABLE 标志来定义该嵌套宏以记录某些内容或不执行任何操作。
      【解决方案6】:

      我会考虑示例宏滥用有两个原因: (1) 宏的名称并不清楚它的作用,最明显的是它可能会返回; (2) 宏在语法上不等同于语句。类似代码

      如果(某些条件) CHECK_FREAD(随便); 别的 do_something_else();

      会失败。我的首选更改:

      #define LOG_AND_RET_ERROR(msg) 做 {LOG_ERROR(msg);返回 DCD_BADREAD;} 而(0) if (x==-1) LOG_AND_RET_ERROR(msg);

      【讨论】:

        【解决方案7】:

        好吧,如果您定义一个短函数,那么您必须在每个调用站点输入更多字符。即

        CHECK_FREAD(X, msg)
        

        对比

        if (check_fread(X, msg)) return DCD_BADREAD;
        

        【讨论】:

          【解决方案8】:

          在实现它的空间范围内,它可能还不错(尽管在 C++ 中它通常不被接受)。

          让我担心的最明显的事情是一些新引入的开发人员可能会看到并考虑这样做

          if (CHECK_FREAD(...)) {
             /* do stuff here */
          } else {
             /* error handle */
          }
          

          这显然是错误的。

          此外,除非在 DCD_BADREAD 中消耗/预期,否则似乎不会使用 msg,这会使情况恶化......

          【讨论】:

          • 新引入的开发人员会犯一些愚蠢的错误,例如使用函数的返回值(或表达式的结果)而不去查找它的实际含义。试图解释这一点没有多大意义。
          • @Steve:另一方面,向他们展示特别令人困惑的宏似乎不友好且适得其反。尤其是当宏格式错误且文档具有误导性时。
          • 没错,但不管它是一个好的宏还是一个坏的(我同意这是一个坏的),同样可以说 any 宏展开给出一个表达式:“如果有人写if (MACRO(...)) { ... } else { ... },那么它就会出错”。
          猜你喜欢
          • 1970-01-01
          • 2011-01-14
          • 2012-06-13
          • 2010-12-04
          • 1970-01-01
          • 1970-01-01
          • 2010-09-06
          • 2010-12-05
          • 2012-06-02
          相关资源
          最近更新 更多