繁体   English   中英

在代码中使用#ifdef是不好的做法吗?

[英]Is it a bad practice to use #ifdef in code?

我必须使用#ifdef i386x86_64作为特定于体系结构的代码,有时需要#ifdef MAC或#ifdef WIN32 ...等等,以便进行特定于平台的代码。

我们必须保持公共代码库和便携式。

但我们必须遵循使用#ifdef严格禁止的指导原则。 我不明白为什么?

作为这个问题的扩展,我还想了解何时使用#ifdef?

例如,dlopen()在从64位进程运行时无法打开32位二进制,反之亦然。 因此它更具体的架构。 在这种情况下我们可以使用#ifdef吗?

使用#ifdef而不是编写可移植代码,您仍然在编写多个特定于平台的代码。 不幸的是,在许多(大多数?)情况下,您很快就会得到几乎无法穿透的便携式和平台特定代码。

您还经常将#ifdef用于可移植性以外的目的(定义要生成的代码的“版本”,例如将包括什么级别的自诊断)。 不幸的是,这两者经常相互作用,并且交织在一起。 例如,有人将一些代码移植到MacOS决定它需要更好的错误报告,他补充说 - 但它使其特定于MacOS。 后来,其他人决定更好的错误报告在Windows上非常有用,所以如果定义了WIN32,他会自动#define ing MACOS启用该代码 - 但随后添加“只需更多” #ifdef WIN32以排除某些代码在定义Win32时,这确实 MacOS特有的。 当然,我们还补充说MacOS基于BSD Unix这一事实,因此当定义MACOS时,它也会自动定义BSD_44 - 但是(再次)转向并在编译MacOS时排除一些BSD“东西”。

这很快退化为代码,如下例所示(取自#ifdef Considered Harmful ):

#ifdef SYSLOG
#ifdef BSD_42
openlog("nntpxfer", LOG_PID);
#else
openlog("nntpxfer", LOG_PID, SYSLOG);
#endif
#endif
#ifdef DBM
if (dbminit(HISTORY_FILE) < 0)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"couldn’t open history file: %m");
#else
    perror("nntpxfer: couldn’t open history file");
#endif
    exit(1);
}
#endif
#ifdef NDBM
if ((db = dbm_open(HISTORY_FILE, O_RDONLY, 0)) == NULL)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"couldn’t open history file: %m");
#else
    perror("nntpxfer: couldn’t open history file");
#endif
    exit(1);
}
#endif
if ((server = get_tcp_conn(argv[1],"nntp")) < 0)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"could not open socket: %m");
#else
    perror("nntpxfer: could not open socket");
#endif
    exit(1);
}
if ((rd_fp = fdopen(server,"r")) == (FILE *) 0){
#ifdef SYSLOG
    syslog(LOG_ERR,"could not fdopen socket: %m");
#else
    perror("nntpxfer: could not fdopen socket");
#endif
    exit(1);
}
#ifdef SYSLOG
syslog(LOG_DEBUG,"connected to nntp server at %s", argv[1]);
#endif
#ifdef DEBUG
printf("connected to nntp server at %s\n", argv[1]);
#endif
/*
* ok, at this point we’re connected to the nntp daemon
* at the distant host.
*/

这是一个相当小的例子,只涉及一些宏,但阅读代码已经很痛苦了。 我亲眼看到(并且不得不处理)在实际代码中糟糕。 这里的代码很丑陋,阅读起来很痛苦,但要弄清楚在什么情况下使用哪些代码仍然相当容易。 在许多情况下,您最终会得到更复杂的结构。

为了给出一个具体的例子,我希望看到这些内容,我会做这样的事情:

if (!open_history(HISTORY_FILE)) {
    logerr(LOG_ERR, "couldn't open history file");
    exit(1);
}

if ((server = get_nntp_connection(server)) == NULL) {
    logerr(LOG_ERR, "couldn't open socket");
    exit(1);
}

logerr(LOG_DEBUG, "connected to server %s", argv[1]);

在这种情况下,它可能是我们的LOGERR的定义是不是一个实际的功能的宏。 它可能是非常微不足道的,有一个标题,如下所示:

#ifdef SYSLOG
    #define logerr(level, msg, ...) /* ... */
#else
    enum {LOG_DEBUG, LOG_ERR};
    #define logerr(level, msg, ...) /* ... */
#endif

[目前,假设一个可以/将处理可变参数宏的预处理器]

鉴于你的主管的态度,即使这可能是不可接受的。 如果是这样,那没关系。 而是一个宏,而不是在函数中实现该功能。 在其自己的源文件中隔离函数的每个实现,并构建适合目标的文件。 如果您有很多特定于平台的代码,您通常希望将其隔离到自己的目录中,很可能使用自己的makefile 1 ,并且有一个顶层的makefile,它可以选择基于其调用的其他makefile。指定目标。


  1. 有些人不喜欢这样做。 我并没有真正地讨论如何构建makefile,只是注意到某些人发现/认为有用的可能性。

你应该尽可能避免使用#ifdef IIRC,是Scott Meyers用#ifdef写的,你不会得到与平台无关的代码。 相反,您会获得依赖于多个平台的代码。 #define#ifdef也不是语言本身的一部分。 #define没有范围的概念,这可能会导致各种各样的问题。 最好的方法是将预处理器的使用保持在最低限度,例如包含保护。 否则你很可能会陷入混乱,这很难理解,维护和调试。

理想情况下,如果需要具有特定于平台的声明,则应该具有单独的特定于平台的包含目录,并在构建环境中适当地处理它们。

如果您具有某些功能的特定于平台的实现,则还应将它们放入单独的.cpp文件中,并在构建配置中再次将它们哈希。

另一种可能性是使用模板。 您可以使用空虚拟结构表示您的平台,并将其用作模板参数。 然后,您可以将模板专门化用于特定于平台的代码。 这样,您将依赖编译器从模板生成特定于平台的代码。

当然,任何这种方法的唯一方法就是将特定于平台的代码非常干净地分解为单独的函数或类。

我看过#ifdef 3个广泛用法:

  • 隔离平台特定代码
  • 隔离功能特定代码(并非所有版本的编译器/语言方言都相同)
  • 隔离编译模式代码( NDEBUG任何人?)

每个都有可能产生大量无法造成的代码,应该相应地对待,但并非所有这些都可以以相同的方式处理。


1.平台特定代码

每个平台都有自己的一套特定的包含,结构和功能来处理IO(主要)等事情。

在这种情况下,处理这种混乱的最简单方法是提供统一的前端,并具有特定于平台的实现。

理想的情况是:

project/
  include/namespace/
    generic.h
  src/
    unix/
      generic.cpp
    windows/
      generic.cpp

这样,平台的东西全部保存在一个文件中(每个标题),因此很容易找到。 generic.h文件描述了接口, generic.cpp由构建系统选择。 没有#ifdef

如果您需要内联函数(用于性能),那么提供内联定义和特定平台的特定genericImpl.i可以包含在generic.h文件的末尾,并带有一个#ifdef


2.功能特定代码

这有点复杂,但通常只有图书馆才有。

例如, Boost.MPL具有可变参数模板的编译器更容易实现Boost.MPL

或者,支持移动构造函数的编译器允许您定义某些操作的更高效版本。

这里没有天堂。 如果你发现自己处于这样的境地......你最终会得到类似Boost的文件(aye)。


3.编译模式代码

你通常可以通过一对#ifdef来逃避。 传统的例子是assert

#ifdef NDEBUG
#  define assert(X) (void)(0)
#else // NDEBUG
#  define assert(X) do { if (!(X)) { assert_impl(__FILE__, __LINE__, #X); } while(0)
#endif // NDEBUG

然后,宏本身的使用不受编译模式的影响,因此至少混乱包含在单个文件中。

注意:这里有一个陷阱,如果在“ifdefed away”时宏没有扩展到某个语句,那么你有可能在某些情况下改变流量。 此外,当混合中存在函数调用(具有副作用)时,不评估其参数的宏可能导致奇怪的行为,但是在这种情况下这是期望的,因为所涉及的计算可能是昂贵的。

许多程序使用这样的方案来制作特定于平台的代码。 更好的方法,也是一种清理代码的方法,是将特定于一个平台的所有代码放在一个文件中,命名函数相同并具有相同的参数。 然后,您只需根据平台选择要构建的文件。

它可能仍然是一些你无法将平台特定代码提取到单独的函数或文件中的地方,你仍然可能需要#ifdef部分,但希望它应该被最小化。

就个人而言,我更愿意将噪音很好地抽象出来(必要时)。 如果这是一个班级界面的全部 - 哎呀!

所以,假设有一个平台定义的类型:

我将使用高级别的typedef作为内部位并创建一个抽象 - 通常每个#ifdef / #else / #endif一行。

然后,对于实现,在大多数情况下,我还将使用单个#ifdef进行抽象(但这确实意味着平台特定的定义在每个平台上出现一次)。 我还将它们分成单独的平台特定文件,这样我就可以通过将所有源代码放入项目并在没有打嗝的情况下构建来重建项目。 在这种情况下, #ifdef比根据每个构建类型计算每个平台的每个项目的所有依赖项更方便。

因此,只需使用它来专注于您需要的平台特定抽象,并使用抽象,因此客户端代码是相同的 - 就像减少变量的范围;)

不确定你的意思是“#ifdef严格禁止”,但也许你指的是你正在进行的项目的政策。

不过,您可能会考虑不检查Mac或WIN32或i386等内容。 一般情况下,如果您使用的是Mac,则实际上并不在意。 相反,您需要MacOS的某些功能,而您关心的是该功能的存在(或不存在)。 出于这个原因,通常在构建设置中有一个脚本,它根据系统提供的功能检查功能和#defines事物,而不是根据平台对功能的存在进行假设。 毕竟,您可能会认为MacOS上没有某些功能,但有人可能拥有一个MacOS版本,他们已经移植了该功能。 检查此类功能的脚本通常称为“configure”,它通常由autoconf生成。

其他人已经指出了首选解决方案:将依赖代码放在一个单独的文件中,该文件包含在内。 这对应于不同实现的文件可以位于单独的目录中(其中一个通过调用中的-I/I指令指定),或者通过动态构建文件的名称(使用例如宏连接) ),并使用类似的东西:

#include XX_dependentInclude(config.hh)

(在这种情况下, XX_dependentInclude可能被定义为:

#define XX_string2( s ) # s
#define XX_stringize( s ) XX_string2(s)
#define XX_paste2( a, b ) a ## b
#define XX_paste( a, b ) XX_paste2( a, b )
#define XX_dependentInclude(name) XX_stringize(XX_paste(XX_SYST_ID,name))

在编译器调用中使用-D/D初始化SYST_ID 。)

在上述所有情况中,将XX_替换为您通常用于宏的前缀。

我更喜欢将平台相关的代码和功能拆分为单独的翻译单元,并让构建过程决定使用哪些单元。

由于拼写错误的标识符,我已经失去了一周的调试时间。 编译器不会跨翻译单元检查已定义的常量。 例如,一个单元可以使用“WIN386”和另一个“WIN_386”。 平台宏是维护的噩梦。

此外,在阅读代码时,您必须检查构建说明和头文件以查看定义了哪些标识符。 存在的标识符和具有值的标识符之间也存在差异。 一些代码可以测试标识符的存在,而另一个代码测试相同标识符的值。 未指定标识符时,后一个测试未定义。

只要相信他们是邪恶的,不愿意使用它们。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM