【问题标题】:Cleaning up Legacy Code "header spaghetti"清理遗留代码“标题意大利面条”
【发布时间】:2008-09-21 07:02:26
【问题描述】:

清理“标题意大利面条”的任何推荐做法,这会导致极度 编译时间慢(Linux/Unix)?

在 GCC 中是否有与“#pragma once”等价的方法?
(发现与此相关的相互矛盾的消息)

谢谢。

【问题讨论】:

    标签: c++ legacy-code


    【解决方案1】:

    假设您熟悉“包含保护”(#ifdef 在标头开头..),另一种加快构建时间的方法是使用外部包含保护。 它在“Large Scale C++ Software Design”中进行了讨论。这个想法是经典的包含守卫,与#pragma once 不同,不要让您从第二次开始忽略标头所需的预处理器解析(即,它仍然必须解析并查找包含守卫的开始和结束。用外部包含保护您将#ifdef 放在#include 行本身周围。

    所以它看起来像这样:

    #ifndef MY_HEADER
    #include "myheader.h"
    #endif
    

    当然在 H 文件中你有经典的包含保护

    #ifndef MY_HEADER
    #define MY_HEADER
    
    // content of header
    
    #endif
    

    这样,myheader.h 文件甚至不会被预处理器打开/解析,它可以在大型项目中为您节省大量时间,尤其是当头文件位于共享的远程位置时,因为它们有时会这样做。

    再一次,一切都在那本书里。 hth

    【讨论】:

    • 我对它们的唯一问题是它们看起来很糟糕。
    • 并且他们添加了一个隐藏的依赖,也就是说,通过外部保护,文件知道保护其包含文件的定义。但是如果你有办法确保你的守卫是唯一生成的(比如使用项目和文件的完整名称),那么这应该不是问题。
    • 外部#include 守卫使阅读和重新组织#include 指令列表变得更加困难,并且拼写错误很快就会出现。我发现他们浪费的开发时间比节省的构建时间要多,尤其是在使用分布式构建系统时。
    【解决方案2】:

    如果你想做一个完整的清理并有时间去做,那么最好的解决方案是删除所有文件中的所有#includes(除了明显的,例如abc.cpp中的abc.h)然后编译该项目。添加必要的前向声明或标头以修复第一个错误,然后重复直到您干净地完成。

    这并不能解决可能导致包含问题的潜在问题,但它确实确保了唯一的包含是必需的。

    【讨论】:

    • 您的解决方案很好,但很耗时。我不知道为什么它被否决了。 +1。
    【解决方案3】:

    我了解到 GCC 认为 #pragma once 已弃用,尽管即使是 #pragma once 也只能做很多事情来加快速度。

    要尝试解开#include 意大利面条,您可以查看doxygen。它应该能够生成包含标题的图表,这可能会给您简化事情的优势。我无法立即回忆起详细信息,但图形功能可能需要您安装 GraphViz 并告诉 doxygen 它可以找到 GraphViz 的 dotty.exe 的路径。

    如果编译时间是您最关心的问题,您可能会考虑的另一种方法是设置 Precompiled Headers

    【讨论】:

      【解决方案4】:

      前几天我读到了一个减少标题依赖的巧妙技巧:编写一个脚本

      • 查找所有#include 语句
      • 一次删除一条语句并重新编译
      • 如果编译失败,重新添加include语句

      最后,希望您的代码中包含最少的必需包含项。您可以编写一个类似的脚本,重新排列包含以找出它们是否自给自足,或者要求在它们之前包含其他标头(首先包含标头,看看编译是否失败,报告它)。这应该有助于清理您的代码。

      还有一些注意事项:

      • 现代编译器(其中包括 gcc)识别标头保护,并以与 pragma once 相同的方式进行优化,只打开文件一次。
      • 当同一个文件在文件系统中具有不同的名称(即使用软链接)时,pragma once 可能会出现问题

      • gcc 支持 #pragma 一次,但称其为“过时”
      • 并非所有编译器都支持pragma once,也不是 C 标准的一部分

      • 不仅编译器可能有问题。 Incredibuild 之类的工具也存在与 #pragma once 相关的问题

      【讨论】:

        【解决方案5】:

        Richard 有点正确(为什么他的解决方案被记录下来?)。

        无论如何,所有 C/C++ 头文件都应该使用内部包含保护。

        这表示,要么:

        1 - 您的遗留代码不再真正得到维护,您应该使用预编译的标头(这是一种 hack,但是嘿……您需要加快编译速度,而不是重构未维护的代码)

        2 - 您的旧代码仍然有效。然后,您可以使用预编译的头文件和/或防护/外部防护作为临时解决方案,但最后,您需要删除所有包含,一次一个 .C 或 .CPP,并编译每个 . C 或 .CPP 文件一次一个,在必要时使用前向声明或包含来更正它们的包含(或者甚至将大型包含分成较小的包含以确保每个 .C 或 .CPP 文件将只获得它需要的标题)。无论如何,测试和删除过时的包含是项目维护的一部分,所以...

        我自己对预编译头文件的体验并不完全是好的,因为有一半时间,编译器找不到我定义的符号,所以我尝试了完整的“清理/重建”,以确保它不是已过时的预编译头文件。所以我的猜测是将它用于你甚至不会接触的外部库(比如 STL、C API 头文件、Boost 等等)。不过,我自己的经验是使用 Visual C++ 6,所以我猜(希望?)他们现在做对了。

        现在,最后一件事:标题应该始终是自给自足的。这意味着如果标题的包含取决于包含的顺序,那么您就有问题了。例如,如果你可以写:

        #include "AAA.hpp"
        #include "BBB.hpp"
        

        但不是:

        #include "BBB.hpp"
        #include "AAA.hpp"
        

        因为 BBB 依赖于 AAA,所以你所拥有的只是一个你从未在代码中承认的依赖关系。不通过定义来确认它只会让你的编译成为一场噩梦。 BBB 也应该包含 AAA(即使它可能会慢一些:最后,前向声明无论如何都会清除无用的包含,因此您应该有一个更快的编译计时器)。

        【讨论】:

          【解决方案6】:

          使用其中一种或多种来加快构建时间

          1. 使用预编译头文件
          2. 使用缓存机制(例如 scons)
          3. 使用分布式构建系统(distcc、Incredibuild($))

          【讨论】:

            【解决方案7】:

            在标头中:仅当您不能使用前向声明时才包含标头,但始终#include 您需要的任何文件(包含依赖项是邪恶的!)。

            【讨论】:

              【解决方案8】:

              正如另一个答案中提到的,您绝对应该尽可能使用前向声明。据我所知,GCC 没有任何与 #pragma once 等价的东西,这就是为什么我坚持包含守卫的旧时尚风格。

              【讨论】:

                【解决方案9】:

                感谢您的回复,但问题是关于现有代码,其中包括严格的“包含顺序”等。 问题是是否有任何工具/脚本来澄清实际发生的事情。

                标头保护不是解决方案,因为它们不会阻止编译器一次又一次地读取整个文件......

                【讨论】:

                • 1 - 您应该编辑/更新原始问题... 2 - 如果您有严格的包含订单,那么您的包含未实现隐藏的依赖项。这不会被工具/脚本捕获。 3 - 就像 rubancache 写的一样,Doxygen+Grapviz 可以制作图表或您的包含。
                【解决方案10】:

                PC-Lint 将大大有助于清理意大利面条标题。它还可以为您解决其他问题,例如未初始化的变量看不见等。

                【讨论】:

                  【解决方案11】:

                  正如 onebyone.livejournal.com 在回复您的问题时评论的那样,一些编译器支持include guard optimization,我链接的页面定义如下:

                  包含保护优化是当编译器识别出上述内部包含保护习惯并采取措施避免多次打开文件时。编译器可以查看包含文件,去除 cmets 和空白,并确定整个文件是否在包含保护中。如果是,它将文件名和包含保护条件存储在映射中。下次要求编译器包含文件时,它可以检查包含保护条件并决定是跳过文件还是#include 它而不需要打开文件。

                  然后,您已经回答了外部包含守卫不是您问题的答案。对于必须以特定顺序包含的头文件,我建议如下:

                  • 每个.c.cpp 文件应首先#include 对应的.h 文件,其余#include 指令应按字母顺序排序。当这破坏了头文件之间未声明的依赖关系时,通常会出现构建错误。
                  • 如果您有一个为基本类型定义全局类型定义的头文件或用于大部分代码的全局#define 指令,则每个.h 文件应首先#include 该文件,其余的@ 987654330@ 指令应按字母顺序排序。
                  • 当这些更改导致编译错误时,您通常必须以#include 的形式将显式依赖项从一个头文件添加到另一个头文件。
                  • 当这些更改不会导致编译错误时,它们可能会导致行为更改。希望您拥有某种可用于验证应用功能的测试套件。

                  听起来问题的一部分可能是增量构建比应有的慢得多。正如其他人所指出的,这种情况可以通过前向声明或分布式构建系统来改善。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-09-16
                    • 2012-01-26
                    • 2011-10-13
                    相关资源
                    最近更新 更多