【问题标题】:Sensible way to write function prototypes编写函数原型的明智方法
【发布时间】:2014-06-10 13:55:52
【问题描述】:

我正在寻找一种(干净的)方法来编写函数定义和函数原型,而无需重复代码。由于 DRY 是一个很好的想法,并且在头文件中手工编码原型显然是违反了这似乎是一个合理的要求。

下面的示例代码表明了一种使用预处理器解决问题的(粗略的)方法。它似乎不太可能是最佳的,但似乎可以正常工作。

使用单独的文件和复制:

foo.h:
#ifndef FOO_H
  #define FOO_H
  // Normal header file stuff
  int dofoo(int a);
#endif /* FOO_H */

foo.c:
#include "foo.h"
int dofoo(int a) {
  return a * 2;
}

使用 C 预处理器:

foo.h:
#ifndef FOO_H
  #define FOO_H

  // Normal header file stuff

  #ifdef PROTOTYPE // if incorrect:
  // No consequences for this test case, but we lose a sanity check
    #error "PROTOTYPE set elsewhere, include mechanism will fall over"
  #endif

  #define PROTOTYPE // if incorrect:
  // "error: redefinition of 'dofoo'" in clang & gcc, 
  // referring to int dofoo() line in foo.c
    #include "foo.c"
  #undef PROTOTYPE //if incorrect:
  // No warnings, but should trigger the earlier #error statement if
  // this method is used in more than one file

#endif /* FOO_H */

foo.c:
#include "foo.h"

int dofoo (int a)
#ifdef PROTOTYPE // if incorrect:
// "error: redefinition of 'dofoo'" in clang & gcc, 
// referring to int dofoo() line in foo.c
  ;
#else
  {
    return a * 2;
  }
#endif

机制有点奇怪 - .h 文件通常不包含 .c 文件!包含守卫停止递归。当通过独立的预处理器运行时,它编译干净并且看起来很合理。否则,在整个源代码中嵌入预处理器条件看起来并不好。

我可以想到几种替代方法。

  1. 不用担心代码重复
  2. 更改为自动生成界面的语言
  3. 使用代码生成器(例如 sqlite 的 makeheaders)

代码生成器可以工作,但作为解决小麻烦的解决方案似乎有点矫枉过正。由于 C 在这一点上已经存在超过 25 年,因此希望社区能够就最佳路径达成共识。

感谢您的阅读。

edit:gcc 4.8.2 和 clang 5.1 的编译器警告 弄乱宏语句会产生相当一致的编译器错误消息。缺少#endif(如果函数定义很长,很容易做到)会产生“错误:未终止的#else”或“错误:未终止的条件指令”,两者都指的是#ifdef行。

缺少#else 意味着代码不再有效 C. gcc "error: expected identifier or '(' before '{' token" and clang add "expected function body after function declarator". 两者都指向正确的行号,但都没有暗示缺少#else。

如果结果是致命的,则 PROTOTYPE 拼写错误会产生连贯的消息,如果结果无关紧要,则不会发出警告。编译器警告并不像定义和声明不同时那样具体,但它们可能已经足够具体了。

【问题讨论】:

  • 这看起来像零增益的大量工作。你把DRY 带到了一个不合理的极端。
  • 奇怪但聪明。不过,我不确定我是否会在实践中这样做。
  • 这是否违反 DRY 是有争议的,作为它的主要动机之一(如果你有两次相同的代码,你有一个错误,忘记在重复的代码中修复它,所以该错误未修复)不要保留:编译器将捕获仅部分代码的更改。
  • 从 DRY 的角度来看,第二种方法更糟糕:有 4 次你可以得到 PROTOTYPE 错误。
  • 我很惊讶主要担心宏命令中的错误。 gcc 4.8.2 和 clang 5.1 在没有启用警告标志的情况下捕获由此产生的致命错误,就像它们会捕获过时的原型一样。因此,安全性似乎并没有降低。我大多不喜欢这种方法,因为它在美学上并不令人满意。

标签: c dry


【解决方案1】:

普遍接受的路径是您的选择 1),不用担心,只需编写两次声明即可。

与函数实现相比,来自原型的重复只是一小部分。像您问题中的宏黑客很快变得笨拙并且几乎没有收益。宏机制最终与原始原型一样多的代码,只是现在更难理解正在发生的事情,并且您会收到更多神秘的错误消息。理解重复的琐碎内容被相同数量的更难理解的诡计所取代。

对于普通原型,编译器会在事情不匹配时发出警告,对于这样一个基于宏的解决方案,如果您忘记了 #endif 或其他不匹配的内容,您将很难理解错误。例如,任何在错误中提及foo.c 都可能定义了PROTOTYPE

【讨论】:

  • 编译器警告似乎在任何一种情况下都足够了。不过,不值得费心的一般建议非常有说服力。干杯
  • 除了Visual Studio 不关心静态和外部的不一致。它甚至不能给我一个警告真是太烦人了,但是对于 gcc 中的相同错误,这是一个错误!
【解决方案2】:

我想从另一个角度来看看它。正如我喜欢看到的 DRY 原则,它对于提供逻辑的代码是有意义的,而不是将其视为字面上重复的字符串。

这样它就不会触及声明,因为它们没有引入任何逻辑。当您看到几段代码时,(如执行某些任务)相同,只是参数发生了变化,那么应该避免/重构它。

这就是你实际所做的。您刚刚在代码中引入了一些新的预处理逻辑,即#ifdef PROTOTYPE... #else ... #endif,您将一遍又一遍地重复,只是更改原型和主体。如果你能把它包装成不强制重复分支的东西,我会说它有点好。

但是目前您确实确实在代码中重复了一些逻辑,只是为了消除多个声明,这在您提供的上下文中基本上是无害的。如果您忘记了某些内容,编译器会告诉您某些内容不匹配。是c。

我想说你提出的方法比重复声明更违反它。

【讨论】:

    猜你喜欢
    • 2015-07-13
    • 1970-01-01
    • 2011-08-07
    • 2020-07-16
    • 1970-01-01
    • 2019-03-13
    • 2011-03-15
    • 2012-07-15
    • 2021-03-17
    相关资源
    最近更新 更多