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

新手上路,请多包涵

我必须使用大量#ifdef i386x86_64 来处理特定于架构的代码,有时还要使用#ifdef MAC 或#ifdef WIN32… 等等来处理特定于平台的代码。

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

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

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

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

原文由 RLT 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1k
2 个回答

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

您还经常得到 #ifdef 用于除可移植性之外的其他目的(定义要生成的代码的“版本”,例如将包括什么级别的自我诊断)。不幸的是,两者经常互动,并交织在一起。例如,将一些代码移植到 MacOS 的人认为它需要更好的错误报告,他补充道——但使其特定于 MacOS。后来,其他人认为更好的错误报告在 Windows 上会非常有用,所以他通过自动启用该代码 #define 如果定义了 WIN32 则使用 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]);

在这种情况下,我们对 loggerr 的定义 可能 是一个宏而不是一个实际的函数。它可能是微不足道的,有一个像这样的标题是有意义的:

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

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

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


  1. 有些人不喜欢这样做。我并没有真正争论如何构建makefile,只是指出有些人认为/认为这是有用的可能性。

原文由 Jerry Coffin 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

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

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

当然,要使这一切工作的唯一方法是将特定于平台的代码非常干净地分解为单独的函数或类。

原文由 Dima 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题