我有遗留的 C++ 代码,我应该从中删除未使用的代码。问题是代码库很大。
如何找出从未调用/从未使用过的代码?
有两种未使用的代码:
对于第一种,一个好的编译器可以提供帮助:
-Wunused
(GCC,Clang)应该警告未使用的变量,Clang 未使用的分析器甚至已经增加以警告从未读取的变量(即使使用过)。-Wunreachable-code
(旧 GCC,于 2010 年删除)应该警告从未访问过的本地块(它发生在早期返回或总是评估为 true 的条件下)catch
的块,因为编译器通常无法证明不会引发异常。对于第二种,难度要大得多。静态地,它需要对整个程序进行分析,即使链接时间优化实际上可能会删除死代码,但实际上程序在执行时已经发生了很大的变化,几乎不可能向用户传达有意义的信息。
因此有两种方法:
gcov
. 请注意,在编译期间应传递特定标志以使其正常工作)。您使用一组良好的不同输入(您的单元测试或非回归测试)运行代码覆盖工具,死代码必然在未到达的代码中......所以您可以从这里开始。如果你对这个主题非常感兴趣,并且有时间和倾向自己实际开发一个工具,我建议使用 Clang 库来构建这样一个工具。
因为 Clang 将为您解析代码并执行重载解析,所以您不必处理 C++ 语言规则,您将能够专注于手头的问题。
但是,这种技术无法识别未使用的虚拟覆盖,因为它们可能被您无法推理的第三方代码调用。
对于未使用的整个函数(和未使用的全局变量),GCC 实际上可以为您完成大部分工作,前提是您使用的是 GCC 和 GNU ld。
编译源代码时使用-ffunction-sections
and -fdata-sections
,链接时使用-Wl,--gc-sections,--print-gc-sections
. 链接器现在将列出所有可以删除的函数,因为它们从未被调用过,以及所有从未被引用过的全局变量。
(当然,您也可以跳过该--print-gc-sections
部分,让链接器静默删除函数,但将它们保留在源代码中。)
注意:这只会找到未使用的完整函数,它不会对函数中的死代码做任何事情。从实时函数中的死代码调用的函数也将被保留。
一些 C++ 特有的特性也会导致问题,特别是:
在这两种情况下,虚函数或全局变量构造函数使用的任何东西也必须保留。
另一个需要注意的是,如果您正在构建一个共享库,GCC 中的默认设置将导出共享库中的每个函数,就链接器而言,它会被“使用”。要解决此问题,您需要将默认设置为隐藏符号而不是导出(使用例如-fvisibility=hidden
),然后显式选择需要导出的导出函数。
好吧,如果你使用 g++,你可以使用这个标志-Wunused
根据文件:
每当一个变量在其声明之外未被使用时,每当一个函数被声明为静态但从未定义时,每当一个标签被声明但未使用时,以及每当一个语句计算一个明确未使用的结果时发出警告。
http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html
编辑:这是其他有用的标志-Wunreachable-code
根据文档:
此选项旨在在编译器检测到至少一整行源代码将永远不会执行时发出警告,因为某些条件永远不会满足或因为它是在一个永远不会返回的过程之后。
更新:我在遗留 C/C++ 项目中发现了类似的主题死代码检测
我认为您正在寻找代码覆盖率工具。代码覆盖率工具会在你的代码运行时分析它,它会让你知道哪些代码行被执行了,多少次,以及哪些没有。
您可以尝试给这个开源代码覆盖工具一个机会:TestCocoon - C/C++ 和 C# 的代码覆盖工具。
真正的答案是:你永远无法真正确定。
至少,对于非平凡的案例,你不能确定你已经得到了所有这些。考虑Wikipedia 关于无法访问代码的文章中的以下内容:
double x = sqrt(2);
if (x > 5)
{
doStuff();
}
正如维基百科正确指出的那样,一个聪明的编译器可能能够捕捉到这样的东西。但考虑修改:
int y;
cin >> y;
double x = sqrt((double)y);
if (x != 0 && x < 1)
{
doStuff();
}
编译器会捕捉到这个吗?也许。但要做到这一点,它需要做的不仅仅是sqrt
针对一个恒定的标量值。它必须弄清楚它(double)y
总是一个整数(容易),然后理解sqrt
整数集的数学范围(困难)。一个非常复杂的编译器可能能够为函数执行此操作,或为math.hsqrt
中的每个函数,或任何它可以找出其域的固定输入函数。这变得非常非常复杂,而且复杂性基本上是无限的。您可以继续向编译器添加复杂的层次,但总会有一种方法可以潜入一些对于任何给定输入集都无法访问的代码。
还有一些输入集永远不会被输入。在现实生活中没有意义的输入,或者被其他地方的验证逻辑阻塞。编译器无法知道这些。
这样做的最终结果是,虽然其他人提到的软件工具非常有用,但除非您事后手动检查代码,否则您永远无法确定您是否掌握了所有内容。即使那样,你也永远无法确定你没有错过任何东西。
恕我直言,唯一真正的解决方案是尽可能保持警惕,使用随手可得的自动化,尽可能重构,并不断寻找改进代码的方法。当然,无论如何,这样做是个好主意。
我自己没有使用过它,但是cppcheck声称可以找到未使用的功能。它可能无法解决完整的问题,但它可能是一个开始。
您可以尝试使用Gimple Software 的 PC-lint/FlexeLint。它声称
在整个项目中查找未使用的宏、typedef、类、成员、声明等
我用它进行静态分析,发现它非常好,但我不得不承认我没有用它来专门查找死代码。
我寻找未使用的东西的正常方法是
watch "make 2>&1"
倾向于在 Unix 上解决问题。这是一个有点漫长的过程,但确实会产生良好的效果。
将尽可能多的公共函数和变量标记为私有或受保护而不会导致编译错误,同时尝试重构代码。通过将函数设为私有并在某种程度上受到保护,您减少了搜索区域,因为私有函数只能从同一个类中调用(除非有愚蠢的宏或其他技巧来规避访问限制,如果是这种情况,我会推荐你找一份新工作)。确定您不需要私有函数要容易得多,因为只有您当前正在处理的类才能调用此函数。如果您的代码库具有小类并且松散耦合,则此方法更容易。如果您的代码库没有小类或耦合非常紧密,我建议先清理它们。
接下来将标记所有剩余的公共函数并制作调用图以找出类之间的关系。从这棵树上,试着找出树枝的哪一部分看起来可以修剪。
这种方法的优点是您可以在每个模块的基础上进行,因此当您的代码库损坏时,很容易保持通过您的单元测试,而无需花费大量时间。
如果你在 Linux 上,你可能想研究一下callgrind
,一个 C/C++ 程序分析工具,它是valgrind
套件的一部分,它还包含检查内存泄漏和其他内存错误的工具(你也应该使用)。它分析程序的运行实例,并生成有关其调用图以及调用图上节点的性能成本的数据。它通常用于性能分析,但它也为您的应用程序生成调用图,因此您可以查看调用了哪些函数以及它们的调用者。
这显然是对页面其他地方提到的静态方法的补充,它只会有助于消除完全未使用的类、方法和函数——它并不能帮助在实际调用的方法中找到死代码。
我真的没有使用任何工具来做这样的事情......但是,据我在所有答案中看到的,没有人说过这个问题是不可计算的。
我这是什么意思?计算机上的任何算法都无法解决这个问题。这个定理(不存在这样的算法)是图灵停机问题的推论。
您将使用的所有工具都不是算法,而是启发式(即不是精确算法)。他们不会为您提供所有未使用的代码。
一种方法是使用调试器和编译器功能,在编译期间消除未使用的机器代码。
一旦消除了某些机器代码,调试器将不允许您在相应的源代码行上放置一个断点。因此,您将断点放在任何地方并启动程序并检查断点 - 那些处于“没有为此源加载代码”状态的断点对应于已消除的代码 - 要么该代码从未被调用,要么它已被内联,你必须执行一些最低限度分析以找出这两个中的哪一个发生了。
至少这就是它在 Visual Studio 中的工作方式,我猜其他工具集也可以做到这一点。
这是很多工作,但我想比手动分析所有代码要快。
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] }
这取决于您用于创建应用程序的平台。
例如,如果您使用 Visual Studio,则可以使用.NET ANTS Profiler 之类的工具,该工具能够解析和分析您的代码。这样,您应该很快知道实际使用了代码的哪一部分。Eclipse 也有等效的插件。
否则,如果您需要知道最终用户实际使用了应用程序的哪些功能,并且如果您可以轻松发布应用程序,则可以使用日志文件进行审核。
对于每个主要功能,您可以跟踪其使用情况,几天/一周后,只需获取该日志文件并查看它。
我有一个朋友今天问了我这个问题,我环顾了一些有前途的 Clang 开发,例如ASTMatcher和静态分析器,它们可能在编译过程中具有足够的可见性以确定死代码部分,但后来我发现这个:
https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables
这几乎是对如何使用一些似乎是为识别未引用符号而设计的 GCC 标志的完整描述!
是否会调用某个函数的一般问题是 NP-Complete。您无法以一般方式提前知道是否会调用某个函数,因为您不知道图灵机是否会停止。如果有一些路径(静态)从 main() 到您编写的函数,您可以获得,但这并不保证您会调用它。
GNU 链接器有一个--cref
产生交叉引用信息的选项。您可以gcc
从命令行通过-Wl,--cref
.
例如,假设foo.o
定义了一个符号,该符号foo_sym
也用于bar.o
. 然后在输出中你会看到:
foo_sym foo.o
bar.o
如果foo_sym
仅限于foo.o
,那么您将看不到任何其他目标文件;后面会跟着另一个符号:
foo_sym foo.o
force_flag options.o
现在,从这个我们不知道foo_sym
是不是用的。它只是一个候选者:我们知道它在一个文件中定义,而没有在任何其他文件中使用。foo_sym
可以在foo.o
那里定义和使用。
所以,你对这些信息所做的是
static
,就像它应该有的那样。当然,我忽略了其中一些符号被故意使用的可能性,因为它们是为动态链接而导出的(即使链接了可执行文件也可能是这种情况);这是一个更微妙的情况,您必须了解并明智地处理。
好吧,如果你使用 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.