我怎么知道代码中的哪些部分从未使用过?


312

我有旧的C ++代码,应该从中删除未使用的代码。问题是代码库很大。

如何找出从未调用/从未使用过的代码?


4
我认为代码查询语言可以使您更好地了解整个项目。我不确定c ++世界,但似乎有cppdepend.com(这不是免费的),看起来还不错。可能类似这样的东西可能是免费提供的。另一件事是,在进行任何形式的重构之前,理智的事情是如果您现在还没有,请进行单元测试。使用单元测试,您可以做的是让代码覆盖率工具对您的代码进行概要分析,如果您无法覆盖该代码,则它本身将有助于删除无效的代码。
Biswanath



3
是的,C ++的有趣之处之一是,删除“未使用”的功能仍可能会改变程序的结果。
MSalters 2011年

1
@MSalters:那是一个有趣的事情……鉴于此,我们必须要讨论为给定调用选择了过载集中的哪个函数,对吗?据我所知,如果有2个功能并重命名f(),并调用f()明确地解析为1,那么它是不可能仅仅通过添加一个名为第3功能,使该呼叫决心2号f()- “最坏的,你可以做通过添加第3个函数将导致调用变得模棱两可,从而阻止程序编译。很想(=感到恐惧)看到一个反例。
j_random_hacker 2011年

Answers:


197

有两种未使用的代码:

  • 局部变量,即在某些函数中,某些路径或变量未使用(或使用,但没有有意义的方式,例如已写入但从未读取)
  • 全局一个:永不调用的函数,永不访问的全局对象

对于第一种,好的编译器可以帮助您:

  • -Wunused(GCC,Clang)应该警告未使用的变量,Clang未使用的分析器甚至已经增加了警告从未读取(即使已使用)的变量。
  • -Wunreachable-code(较旧的GCC,于2010年删除)应警告从未访问的本地块(这种情况发生在早期返回或条件始终为true的情况下)
  • 我没有关于警告未使用catch块的选项,因为编译器通常无法证明不会引发任何异常。

对于第二种,这要困难得多。从静态上讲,它需要整个程序的分析,并且即使链接时间优化实际上可以删除无效代码,但实际上在执行程序时程序已进行了很大的转换,几乎无法向用户传达有意义的信息。

因此,有两种方法:

  • 理论上是使用静态分析器。一款软件,它将立即详细检查整个代码并找到所有流程。在实践中,我不知道任何可行的方法。
  • 实用的方法是使用一种启发式方法:使用代码覆盖率工具(在GNU链中是gcov。请注意,在编译过程中应传递特定的标志,以使其正常工作)。您可以使用一组不错的输入(您的单元测试或非回归测试)来运行代码覆盖率工具,无效代码必然在未到达的代码之内……因此您可以从这里开始。

如果您对该主题非常感兴趣,并且有时间和意愿自己开发一个工具,我建议您使用Clang库来构建这样的工具。

  1. 使用Clang库获取AST(抽象语法树)
  2. 从入口点开始进行标记扫描

因为Clang会为您解析代码并执行重载解析,所以您不必处理C ++语言规则,并且可以专注于手头的问题。

但是,这种技术无法识别未使用的虚拟替代,因为它们可能被您无法推理的第三方代码调用。


7
非常好,+ 1。我喜欢您区分可以静态确定在任何情况下都永远不会运行的代码和不在特定运行中但有可能运行的代码。我认为前者很重要,正如您所说的,使用整个程序的AST进行可达性分析是获得此方法的方法。(防止foo()在仅出现时被标记为“被呼唤” if (0) { foo(); }将是一个奖励,但需要额外的
技巧

@j_random_hacker:也许现在考虑到使用CFG(控制流图)会更好(由于您的示例)。我知道Clang热衷于评论像您提到的重言式比较,因此使用CFG可能会在早期发现死代码。
Matthieu M.

@Matthieu:是的,也许CFG也是我的意思,而不是AST :)我的意思是:一个有向图,其中的顶点是函数,只要x可能调用y,函数x到函数y就有一条边。(并且具有重载函数都由不同的顶点表示的重要属性-听起来像Clang为您做到了,!)
j_random_hacker 2011年

1
@j_random_hacker:实际上,CFG比简单的图更复杂,因为它表示所有要在块中执行的代码,并基于条件语句从一个块到另一个块进行链接。主要优点是,它自然适用于修剪可以静态确定为无效的代码(它会创建可识别的无法访问的块),因此,与AST相比,利用CFG更好地构建您要编写的图谈论...我认为:)
Matthieu M.

1
@j_random_hacker:实际上,Clang的AST确实做到了,它使所有内容都显式(或几乎...),因为它用于处理代码,而不仅仅是用于编译。目前正在进行讨论,因为显然初始化程序列表存在问题,这种隐式转换不会出现在AST中,但我想它将得到解决。
Matthieu M.

35

对于未使用的完整函数(和未使用的全局变量),只要您使用的是GCC和GNU ld,GCC实际上可以为您完成大部分工作。

编译源代码时,请使用-ffunction-sections-fdata-sections,然后在链接使用时-Wl,--gc-sections,--print-gc-sections。链接器现在将列出所有可能被删除的函数,因为它们从未被调用过,而所有全局变量从未被引用过。

(当然,您也可以跳过该--print-gc-sections部分,让链接程序以静默方式删除功能,但将其保留在源代码中。)

注意:这只会找到未使用的完整函数,不会对函数中的无效代码进行任何处理。从活动函数中的死代码调用的函数也将保留。

一些特定于C ++的功能也会引起问题,尤其是:

  • 虚拟功能。如果不知道存在哪些子类以及在运行时实际实例化哪些子类,就无法知道最终程序中需要存在哪些虚函数。链接器没有足够的信息,因此必须保留所有这些信息。
  • 具有构造函数及其构造函数的全局变量。通常,链接器不知道全局构造函数没有副作用,因此必须运行它。显然,这意味着还必须保留全局本身。

在这两种情况下,虚函数或全局变量构造函数使用的所有内容都必须保留。

另一个警告是,如果您要构建共享库,则GCC中的默认设置将导出共享库中的每个函数,从而就链接器而言将其“使用”。要解决此问题,您需要将默认设置为隐藏符号而不是导出(使用例如-fvisibility=hidden),然后显式选择您需要导出的导出函数。


实用的建议。仅仅获得已知在任何地方都不会使用的功能的列表(即使您说的是,这个列表并不完整),我也会从中收获很多。
j_random_hacker 2011年

我认为这些都不适用于未实例化的模板
雅各布·克林科夫斯基

25

好吧,如果您使用g ++,则可以使用此标志 -Wunused

根据文档:

当变量除了声明外没有被使用,函数被声明为静态但从未定义,标签被声明但未使用,语句计算的结果未明确使用时,均会发出警告。

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

编辑:这是其他有用的标志-Wunreachable-code 根据文档:

此选项旨在在编译器检测到至少一整行源代码永远不会执行,因为某些条件从未满足或因为它在过程永不返回之后而发出警告时发出警告。

更新:我在旧的C / C ++项目中发现了类似的主题死代码检测


4
这不会捕获从未调用过的原型函数的标头。或不被调用的公共类方法。它只能检查在该范围内是否使用了局部范围的变量。
法尔玛里2011年

@Falmarri我从未使用过此标志。我试图弄清楚自己能找到什么样的无效代码。
UmmaGumma 2011年

-Wunused警告已声明(或一口气声明和定义)但实际上从未使用过的变量。顺便说一句,范围保护器非常令人讨厌:p Clang中有一个实验性的实现,可以警告已写入但从未读取过的非易失性变量(由Ted Kremenek编写)。-Wunreachable-code警告某个函数中的代码无法到达,例如,它可以是位于throwreturn语句之后的代码,或者是从未使用过的分支中的代码(例如在重言式比较的情况下发生)。
Matthieu M.

18

我认为您正在寻找代码覆盖率工具。一个代码覆盖率工具将在您的代码运行时对其进行分析,并让您知道执行了哪些代码行,执行了多少次,以及哪些没有执行。

您可以尝试给这个开源代码覆盖工具一个机会:TestCocoon -C / C ++和C#的代码覆盖工具。


7
这里的关键是“正在运行”-如果您的输入数据未使用某些代码路径,该路径将不会被识别为已使用,会吗?
sharptooth 2011年

1
那是正确的。如果不运行代码,就无法知道未到达哪些行。我想知道设置一些单元测试来模拟一些正常运行会有多困难。
卡洛斯五世

1
@drhishch我认为,大多数此类未使用的代码必须找到链接器,而不是编译器。
UmmaGumma 2011年

1
@drhirsch是的,编译器可以处理一些无法访问的代码,例如已声明但未被调用的函数以及一些短路评估,但是依赖于用户操作或运行时变量的代码又如何呢?
卡洛斯五世

1
@golcarcol好,让我们void func()在a.cpp中具有功能,该功能 在b.cpp中使用。编译器如何检查func()是否在程序中使用?这是链接器的工作。
UmmaGumma 2011年

15

真正的答案是:您永远不可能真正知道。

至少,对于非平凡的案例,您不能确定是否已全部掌握。考虑一下Wikipedia关于不可达代码的文章中的以下内容:

double x = sqrt(2);
if (x > 5)
{
  doStuff();
}

正如Wikipedia正确指出的那样,一个聪明的编译器也许可以捕获类似的东西。但考虑修改:

int y;
cin >> y;
double x = sqrt((double)y);

if (x != 0 && x < 1)
{
  doStuff();
}

编译器会抓住这个吗?也许。但是要做到这一点,除了在sqrt恒定的标量值上运行之外,还需要做更多的事情。必须弄清楚它(double)y始终是整数(简单),然后了解sqrt整数集的数学范围(硬)。一个非常复杂的编译器可能可以针对该sqrt函数,math.h中的每个函数,或针对其可以确定其范围的任何固定输入函数执行此操作。这变得非常非常复杂,并且复杂性基本上是无限的。您可以继续在编译器中添加复杂性,但是总会有一种方法可以潜入某些对于任何给定输入集都无法访问的代码。

还有一些输入集根本就不会输入。在现实生活中毫无意义的输入,或被其他地方的验证逻辑阻止的输入。编译器无法了解这些内容。

这样做的最终结果是,尽管其他人提到的软件工具非常有用,但是除非您事后手动检查代码,否则您将永远无法确定自己是否捕获了所有内容。即使那样,您也永远不会确定自己没有错过任何东西。

唯一真正的解决方案,恕我直言,要尽可能保持警惕,使用您可以使用的自动化,在可能的地方进行重构,并不断寻找改进代码的方法。当然,无论如何还是一个好主意。


1
正确,不要留下无效代码!如果删除功能,请终止无效代码。“以防万一”将其保留在那里只会导致膨胀(如您所讨论的),以后很难找到。让版本控制为您做ho积。
Lightness Races in Orbit

12

我自己没有使用过它,但是cppcheck声称可以找到未使用的功能。它可能无法解决完整的问题,但这可能是一个开始。


是的,它能够找到本地未引用的变量和函数。
Chugaister

是的cppcheck --enable=unusedFunction --language=c++ .,可以找到这些未使用的功能。
杰森·哈里斯

9

您可以尝试使用Gimple Software提供的PC-lint / FlexeLint。它声称

在整个项目中查找未使用的宏,typedef,类,成员,声明等

我已经将它用于静态分析,并且发现它非常好,但是我不得不承认我没有使用它专门查找无效代码。


5

我发现未使用的东西的正常方法是

  1. 确保构建系统正确处理依赖项跟踪
  2. 设置带有全屏终端窗口的第二个监视器,运行重复的构建并显示第一个屏幕输出。watch "make 2>&1"倾向于在Unix上达到目的。
  3. 在整个源代码树上运行查找和替换操作,在每行的开头添加“ //?”
  4. 通过删除“ //”来修复编译器标记的第一个错误。在相应的行中。
  5. 重复直到没有错误。

这是一个冗长的过程,但确实能带来良好的效果。


2
有优点,但劳动强度大。同样,您必须确保同时取消注释函数的所有重载-如果有多个函数重载,则对不那么受欢迎的函数进行注释可能会使编译成功,但会导致错误的程序行为(以及错误的想法)。功能)。
j_random_hacker 2011年

我只在第一步取消注释(所有重载),然后在下一次迭代中查看缺少的定义;这样,我可以看到实际使用了哪些重载。
Simon Richter

@Simon:有趣的是,在对主要问题的评论中,MSalters指出,即使对于从未调用的函数的声明的存在/不存在,也可能影响通过重载解析找到另外两个函数中的哪个。诚然,这需要极其奇怪和人为的设置,因此在实践中这不太可能成为问题。
j_random_hacker 2011年

4

在不引起编译错误的情况下,将尽可能多的公共函数和变量标记为私有或受保护,同时尝试重构代码。通过将函数设为私有并在某种程度上受保护,您可以缩小搜索范围,因为只能从同一类调用私有函数(除非有愚蠢的宏或其他技巧来规避访问限制,如果是这种情况,我建议您找新工作)。确定是否不需要私有函数要容易得多,因为只有当前正在处理的类才能调用此函数。如果您的代码库具有较小的类并且是松散耦合的,则此方法会更容易。如果您的代码库没有小类或耦合紧密,建议您先清理它们。

接下来将标记所有剩余的公共功能,并创建一个调用图以了解类之间的关系。从这棵树中,尝试找出分支的哪一部分看起来可以修剪。

这种方法的优点是您可以按模块进行操作,因此很容易保持通过单元测试的状态,而不会因代码库损坏而花费大量时间。


3

如果您使用的是Linux,则可能需要研究套件中callgrind的C / C ++程序分析工具valgrind,该工具还包含检查内存泄漏和其他内存错误(也应使用)的工具。它分析程序的正在运行的实例,并生成有关其调用图以及有关调用图上节点的性能成本的数据。它通常用于性能分析,但也会为您的应用程序生成调用图,因此您可以查看调用了哪些函数以及它们的调用方。

显然,这是对页面其他地方提到的静态方法的补充,并且仅对消除完全未使用的类,方法和函数有帮助-它完全无助于在实际调用的方法中查找无效代码。


3

我确实没有使用过任何能做到这一点的工具...但是,据我在所有答案中所看到的,没有人说过这个问题是无可争议的。

我是什么意思 无法通过计算机上的任何算法解决此问题。这个定理(不存在这样的算法)是图灵停止问题的推论。

您将使用的所有工具都不是算法,而是试探法(即,不是精确算法)。他们不会为您提供未使用的所有代码。


1
我认为OP主要是希望找到不会在任何地方调用的函数,这当然也不是没有争议的-大多数现代链接器都可以做到!只需以最少的痛苦和苦恼就提取该信息即可。
j_random_hacker 2011年

您是对的,我没有看到对主要问题的最后评论。顺便说一句,可能有一些代码中未实际使用的功能。可能无法检测到这种情况。
geekazoid 2011年

2

一种方法是使用调试器和编译器功能,以消除编译期间未使用的机器代码。

一旦消除了一些机器代码,调试器将不允许您在相应的源代码行上放一个断点。因此,您到处都放置断点,然后启动程序并检查断点-处于“未为此源加载任何代码”状态的断点对应于已删除的代码-要么从不调用该代码,要么已内联该代码,并且您必须执行一些最小操作分析以查找这两个事件中的哪一个发生了。

至少这是它在Visual Studio中的工作方式,我想其他工具集也可以做到这一点。

这项工作很多,但我想比手动分析所有代码要快。


4
我认为OP的问题在于如何找到源代码的更小,更易于管理的子集,而不是确保编译后的二进制文件高效。
j_random_hacker 2011年

@j_random_hacker我给了它一个机会-事实证明,消除代码甚至可以用于追溯到原始源代码。
Sharptooth 2011年

您必须在Visual Studio上使用一些特定的编译器标志来实现它吗?它仅在发布模式下工作还是在调试中工作?
Naveen

编译器使用但已优化的行如何处理?
Itamar Katz

@Naveen:在Visual C ++ 9中,您必须打开优化并使用/ OPT:ICF
Sharptooth 2011年

2

CppDepend是一种商业工具,可以检测未使用的类型,方法和字段,并执行更多操作。它适用于Windows和Linux(但目前不支持64位),并附带2周的试用期。

免责声明:我不在那儿工作,但是我拥有该工具的许可证(以及NDepend,后者是.NET代码的更强大替代品)。

对于那些好奇的人,这是一个用CQLinq编写的示例内置(可定制)规则,用于检测无效方法:

// <Name>Potentially dead Methods</Name>
warnif count > 0
// Filter procedure for methods that should'nt be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
    m => !m.IsPublic &&       // Public methods might be used by client applications of your Projects.
         !m.IsEntryPoint &&            // Main() method is not used by-design.
         !m.IsClassConstructor &&      
         !m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
         !(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
           m.IsProtected) &&
         !m.IsGeneratedByCompiler
)

// Get methods unused
let methodsUnused = 
   from m in JustMyCode.Methods where 
   m.NbMethodsCallingMe == 0 && 
   canMethodBeConsideredAsDeadProc(m)
   select m

// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
   methods => // Unique loop, just to let a chance to build the hashset.
              from o in new[] { new object() }
              // Use a hashet to make Intersect calls much faster!
              let hashset = methods.ToHashSet()
              from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
              where canMethodBeConsideredAsDeadProc(m) &&
                    // Select methods called only by methods already considered as dead
                    hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe
              select m)

from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
select new { m, m.MethodsCallingMe, depth = deadMethodsMetric[m] }

更新:在版本3.1中添加了对Linux的64位支持。
罗曼·布科

1

这取决于您用来创建应用程序的平台。

例如,如果使用Visual Studio,则可以使用诸如.NET ANTS Profiler之类的工具,该工具能够解析和分析代码。这样,您应该快速知道实际使用了代码的哪一部分。Eclipse也有等效的插件。

否则,如果您需要了解最终用户实际使用的应用程序功能,并且可以轻松发布应用程序,则可以使用日志文件进行审核。

对于每个主要功能,您都可以跟踪其用法,几天/一周后,只需获取该日志文件,然后对其进行查看。


1
.net ANTS Profiler看起来像是用于C#的-您确定它也适用于C ++吗?
j_random_hacker 2011年

@j_random_hacker:据我所知,它与托管代码一起使用。因此,.net ANTS当然将无法分析“标准” C ++代码(即,用gcc,...编译)。
AUS

0

我认为这不会自动完成。

即使使用代码覆盖工具,您也需要提供足够的输入数据才能运行。

可能是非常复杂且价格昂贵的静态分析工具,例如CoverityLLVM编译器提供的帮助。

但是我不确定,我希望手动检查代码。

更新

好吧..仅删除未使用的变量,但未使用的功能并不难。

更新

在阅读了其他答案和评论后,我更加坚信这是不可能完成的。

您必须知道代码才能具有有意义的代码覆盖率度量,并且如果您知道很多手动编辑将比准备/运行/查看覆盖率结果更快。


2
您的回答的措辞具有误导性,LLVM并不昂贵...它是免费的!
Matthieu M.

手动编辑将无法帮助您通过程序中通过逻辑分支的运行时变量。如果您的代码从未满足特定条件,因此始终遵循相同的路径怎么办?
卡洛斯五世


0

是否将调用某些函数的一般问题是NP-Complete。您无法以一般的方式提前知道是否会调用某些功能,因为您不知道图灵机是否会停止。如果从main()到您编写的函数之间有某种路径(静态),您可以得到,但这并不保证您会被调用。


-3

好吧,如果您使用g ++,则可以使用此标志-Wunused

根据文档:

Warn whenever a variable is unused aside from its declaration, whenever a function is declared static but never defined, whenever a label is declared but not used, and whenever a statement computes a result that is explicitly not used.

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

编辑:这是其他有用的标志-Wunreachable-code根据文档:

This option is intended to warn when the compiler detects that at least a whole line of source code will never be executed, because some condition is never satisfied or because it is after a procedure that never returns.

6
当前最受好评的答案中已经提到了这些确切的信息。请阅读现有答案,以避免不必要的重复。
j_random_hacker 2011年

1
现在,您可以获得您的Peer Pressure徽章!
Andrew Grimm'7
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.