【问题标题】:Is it a bad practice to use #ifdef in code?在代码中使用#ifdef 是一种不好的做法吗?
【发布时间】:2011-12-22 01:16:41
【问题描述】:

我必须使用大量的#ifdef i386 和 x86_64 用于特定于体系结构的代码,有时使用#ifdef MAC 或#ifdef WIN32... 等等特定于平台的代码代码。

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

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

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

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

【问题讨论】:

  • 严格不是什么意思?
  • 所有的概括都是错误的,即使是这个。需要更多上下文:您是使用 #ifdef 来隔离特定于平台或架构的代码(增加可移植性)还是出于其他原因?
  • 我们的领导没有。他不希望该代码应该有#ifdef。
  • 您可能想阅读Ifdef Considered Harmful。
  • 当然这是不好的做法。如果在您不必维护的代码中使用它会更好。与 boost 一样,v1.44 有 831 个包含#ifdef 的文件。

标签: c++ macos coding-style


【解决方案1】:

就个人而言,我更喜欢很好地抽象出噪音(在必要时)。如果它遍布类接口的主体 - 呸!

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

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

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

因此,只需使用它来专注于您需要的特定于平台的抽象,并使用抽象以使客户端代码相同——就像缩小变量的范围一样;)

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 这件事的好处是,无论哪个领导说“#idfef is strict no”,都会把问题抛给他们,因为他们需要决定项目将如何构建其特定于平台的结构代码以及如何配置构建以编译和链接正确的特定于平台的文件,以及正确的特定于平台的包含路径。这让他们有机会解释他们的原则,而不是制定禁令然后飘向远方;-)
    【解决方案3】:

    不确定您所说的“#ifdef is strict no”是什么意思,但也许您指的是您正在从事的项目的政策。

    不过,您可能会考虑不检查 Mac、WIN32 或 i386 之类的东西。通常,您实际上并不关心您是否在 Mac 上。相反,您需要 MacOS 的某些功能,而您关心的是该功能的存在(或不存在)。出于这个原因,通常在您的构建设置中有一个脚本来检查功能并根据系统提供的功能#defines 事物,而不是基于平台对功能的存在做出假设。毕竟,您可能会假设 MacOS 上没有某些功能,但有人可能拥有移植了该功能的 MacOS 版本。检查此类功能的脚本通常称为“配置”,它通常由 autoconf 生成。

    【讨论】:

      【解决方案4】:

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

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

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

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

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

      【讨论】:

      • 来自不同架构的代码呢?假设加载 dylib 二进制文件?它应该与架构分开吗?
      • @MacGeek:什么是 dylib 二进制文件?一个 dll 还是一个 .so?你链接反对它吗?
      • 如果没有#ifdefs,你将如何提供正确的平台虚拟结构?
      • @Xeo 您将拥有一个特定于平台的头文件,其中包含typedef。
      • @MacGeek 那么,如果你链接到一个 dll,那是构建环境的问题。如果您在代码中动态加载它,那么它应该在特定于平台的函数或类中完成。
      【解决方案5】:

      其他人已经指出了首选的解决方案:将依赖代码放入 一个单独的文件,其中包含。这对应的文件 不同的实现可以在不同的目录中(其中之一 通过-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_ 替换为您通常用于宏的前缀。

      【讨论】:

        【解决方案6】:

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

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

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

        只要相信它们是邪恶的,不想使用它们。

        【讨论】:

          【解决方案7】:

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

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

          这很快就会退化为如下示例的代码(取自#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
          

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

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


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

          【讨论】:

            【解决方案8】:

            我见过#ifdef 的三种广泛用法:

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

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


            1.平台特定代码

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

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

            理想情况下:

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

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

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


            2。功能特定代码

            这有点复杂,但通常只有图书馆才能体验到。

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

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

            这里没有天堂。如果你发现自己处于这种情况......你最终会得到一个类似 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”时对语句很重要的东西,那么在某些情况下你可能会改变流程。此外,当混合中存在函数调用(具有副作用)时,不评估其参数的宏可能会导致奇怪的行为,但在这种情况下,这是可取的,因为所涉及的计算可能很昂贵。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-10-03
              • 1970-01-01
              • 2020-04-23
              • 2012-10-12
              • 1970-01-01
              • 1970-01-01
              • 2019-09-09
              相关资源
              最近更新 更多