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

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

本文关键字:是不是 代码 #ifdef      更新时间:2023-10-16

我不得不使用大量的#ifdef i386x86_64架构特定的代码,有时#ifdef MAC或#ifdef WIN32…对于平台特定的代码,

我们必须保持通用代码库和可移植性。

但是我们必须遵循使用#ifdef是严格的准则。我不明白为什么?

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

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

使用#ifdef而不是编写可移植代码,您仍然需要编写多个特定于平台的代码片段。不幸的是,在许多(大多数?)情况下,您很快就会得到一种几乎难以理解的可移植代码和平台特定代码的混合。

您还经常看到#ifdef被用于可移植性以外的目的(定义什么"版本")。要生成的代码(例如将包含什么级别的自诊断)。不幸的是,这两者经常相互影响,交织在一起。例如,有人将一些代码移植到MacOS,认为它需要更好的错误报告,他补充说——但使其特定于MacOS。后来,有人决定更好的错误报告将在Windows上非常有用,所以他通过自动#define MACOS来启用该代码,如果WIN32是定义的——但随后添加了"只是几个";当Win32被定义时,#ifdef WIN32排除一些真正 MacOS特定的代码。当然,我们还添加了MacOS是基于BSD Unix的事实,所以当MacOS被定义时,它也会自动定义BSD_44——但是(再次)反过来,排除了一些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 %sn", 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]);
在这种情况下,有可能我们对logger的定义是一个宏而不是一个实际的函数。这可能是足够琐碎的,有一个标题是有意义的,如:
#ifdef SYSLOG
    #define logerr(level, msg, ...) /* ... */
#else
    enum {LOG_DEBUG, LOG_ERR};
    #define logerr(level, msg, ...) /* ... */
#endif

[目前,假设预处理器可以/将会处理可变宏]

考虑到你上司的态度,即使是那个也可能是不可接受的。如果是这样,那很好。而不是宏,而是在函数中实现该功能。将函数的每个实现隔离在其自己的源文件中,并构建适合目标的文件。如果您有很多特定于平台的代码,您通常希望将其隔离到自己的目录中,很可能有自己的makefile1,并拥有一个顶级makefile,该makefile仅根据指定的目标选择要调用哪些其他makefile。


    有些人不喜欢这样做。我并不是真的在争论如何构建makefile,只是注意到这是一些人认为有用的一种可能性。

您应该尽可能避免使用#ifdef。IIRC,是Scott Meyers写的,使用#ifdef,你不会得到与平台无关的代码。相反,您得到的代码依赖于多个平台。而且#define#ifdef也不是语言本身的一部分。#define没有范围的概念,这可能会导致各种各样的问题。最好的方法是尽量减少预处理器的使用,比如include守卫。否则,你很可能会以一团乱麻告终,这将很难理解、维护和调试。

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

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

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

当然,要做到这一点,唯一的方法是非常清晰地将特定于平台的代码分解成单独的函数或类。

我看到了#ifdef的三种广泛用法:

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

每一个都有可能产生一大堆不可维护的代码,应该相应地处理,但并不是所有的都可以用相同的方式处理。


1。平台专用代码

每个平台都有自己的一套特定的包含、结构和函数来处理诸如IO(主要是)之类的事情。

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

理想:

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

这样,平台的东西都保存在一个文件中(每个头文件),所以很容易定位。generic.h文件描述接口,generic.cpp文件由构建系统选择。No #ifdef .

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


2。功能特定代码

这有点复杂,但通常只有库才会遇到。

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

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

这里没有天堂。如果你发现自己处于这种情况……你最终会得到一个类似boost的文件(是的)。


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部分,但希望它应该最小化。

我更喜欢拆分平台相关代码&特性转换成独立的翻译单元,并让构建过程决定使用哪个单元。

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

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

相信它们是邪恶的,不喜欢使用它们。

不确定你所说的"#ifdef是严格不"是什么意思,但也许你指的是你正在从事的项目的策略。

你可以考虑不检查像Mac或WIN32或i386这样的东西。一般来说,你并不关心你是否在Mac上。相反,你想要MacOS的一些功能,你关心的是该功能的存在(或不存在)。出于这个原因,在构建设置中通常有一个脚本,该脚本根据系统提供的功能检查功能并定义内容,而不是基于平台对功能的存在进行假设。毕竟,您可能会认为MacOS上没有某些功能,但有些人可能在MacOS的某个版本上移植了这些功能。检查这些特性的脚本通常称为"configure",它通常由autoconf生成。

就我个人而言,我更喜欢很好地抽象噪声(必要时)。如果它遍布类接口的整个主体,那就太糟糕了!

那么,假设有一个平台定义的类型:

我将在高层为内部位使用类型定义,并创建一个抽象-通常每个#ifdef/#else/#endif一行。

那么对于实现,在大多数情况下,我也将为该抽象使用单个#ifdef(但这确实意味着平台特定的定义在每个平台上出现一次)。我还将它们分离到单独的特定于平台的文件中,这样我就可以通过将所有源代码放入一个项目中来重新构建一个项目,而不会出现任何问题。在这种情况下,#ifdef也比试图找出每个项目,每个平台,每个构建类型的所有依赖关系更方便。

因此,只需使用它来关注您需要的特定于平台的抽象,并且使用抽象使客户端代码相同——就像减少变量的作用域一样;)

其他人指出了首选的解决方案:将依赖代码放入一个单独的文件,包含在内。这是对应的文件不同的实现可以位于不同的目录中(例如通过-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))

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

在上述所有代码中,将XX_替换为通常用于宏的前缀