将C源代码转换为C++



如何将一个相当大(>300K)、相当成熟的C代码库转换为C++?

我心目中的C类型被划分为大致对应于模块的文件(即,比典型的基于OO类的分解粒度更小),使用内部链接代替私有函数和数据,使用外部链接代替公共函数和数据。全局变量广泛用于模块之间的通信。有一个非常广泛的集成测试套件可用,但没有单元(即模块)级测试。

我有一个总体策略:

  1. 编译C++的C子集中的所有内容并使其发挥作用
  2. 将模块转换为大型类,以便所有交叉引用都由类名限定范围,但将所有函数和数据保留为静态成员,并使其发挥作用
  3. 使用适当的构造函数和初始化的交叉引用将巨大的类转换为实例;将静态成员访问替换为间接访问(视情况而定);让它发挥作用
  4. 现在,将项目作为一个考虑不周的OO应用程序来处理,在依赖关系可处理的地方编写单元测试,在不可处理的时候分解成单独的类;这里的目标是在每次转换时从一个工作程序转移到另一个

显然,这将是一项相当艰巨的工作。有没有关于这种翻译的案例研究/战争故事?替代策略?其他有用的建议?

注1:这个程序是一个编译器,可能有数百万其他程序依赖于它的行为不改变,所以大规模重写几乎不是一种选择。

注2:源代码已有近20年的历史,每年可能有30%的代码流失(修改的行数+添加的行数/以前的总行数)。换言之,它得到了大量的维护和扩展。因此,其中一个目标是增加不可控制性。

[为了这个问题,假设翻译成C++是强制性的,而把它留在C中是而不是的一个选项。添加这个条件的目的是去掉"留在C中"的答案。]

几个月前刚刚开始做同样的事情(在一个有十年历史的商业项目上,最初是以"C++只不过是具有智能struct的C"哲学编写的),我建议使用与吃大象相同的策略:一次咬一口。:-)

尽可能将其分为几个阶段,这些阶段可以在对其他部分影响最小的情况下完成。正如Federico Ramponi所建议的那样,构建一个facade系统是一个良好的开端——一旦所有东西都有了C++facade并通过它进行通信,你就可以非常确定地更改模块的内部,它们不会影响外部的任何东西。

我们已经有了一个部分C++接口系统(由于之前较小的重构工作),所以这种方法在我们的情况下并不困难。一旦我们把所有东西都作为C++对象进行通信(这需要几周的时间,在一个完全独立的源代码分支上工作,并在批准后将所有更改集成到主分支中),我们很少在出发前编译出一个完全可工作的版本。

转换还没有完成——我们已经暂停了两次临时发布(我们的目标是每几周发布一次),但进展顺利,没有客户抱怨任何问题。我们的QA人员只发现了一个问题,我也记得。:-)

关于:

  1. 编译C++的C子集中的所有内容并使其发挥作用,以及
  2. 实现一组保持C代码不变的外观

为什么"翻译成C++是强制性的"?您可以包装C代码,而无需将其转换为大型类等。

您的应用程序有很多人在处理它,不需要破坏它。如果你真的想大规模转换为OO风格您需要大量的转换工具来实现工作自动化。

基本思想是将数据组指定为类,然后让工具重构代码,将数据移动到类中,将该数据上的函数移动到这些类中,并将对该数据的所有访问修改为对类的调用。

你可以做一个自动的预分析来形成统计集群来获得一些想法,但你仍然需要一位了解应用程序的工程师来决定数据元素应该分组。

能够完成这项任务的工具是我们的DMS软件重组工具包。DMS有强大的C解析器来读取代码,捕获C代码作为编译器的抽象语法树,(与传统编译器不同)可以计算整个300K SLOC的流量分析。DMS有一个C++前端,可以用作"后端";编写将C语法映射到C++语法的转换。

一个大型航空电子系统的C++重组任务给出了对这类活动使用DMS的一些想法。请参阅技术文件www.semdesigns.com/Products/DMS/DMSToolkit.html,明确地通过自动程序转换重新设计C++组件模型

这个过程不适合胆小的人。但是比任何人考虑对大型应用程序进行手动重构已经不怕辛苦了。

是的,我和这家公司有关系,是它的首席架构师。

我会在C接口上编写C++类。不接触C代码将减少出错的机会,并显著加快进程。

一旦你有了C++接口;然后,将代码复制+粘贴到类中是一项琐碎的任务。正如您所提到的,在这个步骤中,进行单元测试是至关重要的。

GCC目前正处于从C到C++的中间转换阶段。显然,他们首先将所有内容移动到C和C++的公共子集中。在这样做的过程中,他们在GCC中添加了对他们在-Wc++-compat下发现的所有内容的警告。这会让你踏上旅程的第一段。

对于后面的部分,一旦您真正使用C++编译器编译了所有内容,我将专注于替换具有惯用C++对应项的内容。例如,如果您使用的是使用C宏定义的列表、映射、集合、位向量、哈希表等,那么通过将它们移到C++中,您可能会获得很多好处。同样,使用OO,您可能会发现已经在使用C OO习惯用法(如结构继承)的好处,以及C++将为代码提供更清晰和更好的类型检查的好处。

您的列表看起来不错,只是我建议您先查看测试套件,并在进行任何编码之前尽可能紧凑。

让我们抛出另一个愚蠢的想法:

  1. 编译C++的C子集中的所有内容并使其发挥作用
  2. 从一个模块开始,在一个巨大的类中转换它,然后在一个实例中转换,并从该实例构建一个C接口(与您开始使用的接口相同)。让剩下的C代码使用该C接口
  3. 根据需要进行重构,一次一个模块地从C代码中扩展OO子系统,并在C接口的某些部分变得无用时丢弃它们

除了想如何开始之外,可能还有两件事需要考虑,那就是你想focus什么,以及你想在哪里stop

您表示存在大量代码流失,这可能是集中精力的关键。我建议你选择代码中需要大量维护的部分,成熟/稳定的部分显然工作得足够好,所以最好保持原样,除了一些带有外墙的装饰。

您想在哪里停止取决于想要转换为C++的原因。这本身很难成为一个目标。如果是由于某些第三方依赖,请将精力集中在该组件的接口上。

我工作的软件是一个巨大的、旧的代码库,几年前就已经从C"转换"到了C++。我认为这是因为GUI被转换为Qt。即使是现在,它仍然主要看起来像一个带有类的C程序。打破由公共数据成员引起的依赖关系,并将具有过程性怪物方法的巨大类重构为更小的方法和类,我认为这从未真正成功,原因如下:

  1. 不需要更改正在运行且不需要增强的代码。这样做会在不添加功能的情况下引入新的错误,而最终用户对此并不欣赏
  2. 要可靠地进行重构是非常非常困难的。许多代码块太大,也太重要,以至于人们几乎不敢碰它。我们有一套相当广泛的功能测试,但很难获得足够的代码覆盖率信息。因此,很难确定是否已经有足够的测试来检测重构过程中的问题
  3. 投资回报率很难确定。最终用户不会从重构中受益,因此它必须降低维护成本,而维护成本最初会增加,因为通过重构,您会在成熟的代码中引入新的错误,即相当无错误的代码。而且重构本身也将耗资巨大

注:。我想你知道"有效地使用遗留代码"这本书吧?

您提到您的工具是一个编译器,并且:"实际上,在多重调度中,模式匹配,而不仅仅是类型匹配会更好"。

你可能想看看maketea。它为AST提供模式匹配,以及来自抽象语法的AST定义,以及访问者、转换器等。

如果你有一个小型或学术项目(比如少于10000行),重写可能是你的最佳选择。你可以随心所欲地考虑它,而且不会花太多时间。

如果您有一个真实世界的应用程序,我建议将其编译为C++(这通常意味着主要修复函数原型等),然后进行重构和OO封装。当然,我并不认同这样一种哲学,即代码必须是OO结构的,才能成为可接受的C++代码。我会根据您的需要进行逐段的转换、重写和重构(对于功能或合并单元测试)。

以下是我要做的:

  • 由于该代码已有20年的历史,请废弃解析器/语法分析器,代之以更新的基于lex/yacc/bison(或任何类似的)等的C++代码,这些代码更易于维护和理解。如果你手边有BNF,开发速度也会更快
  • 一旦对旧代码进行了修改,就开始将模块封装到类中。用接口替换全局/共享变量
  • 现在,您将拥有一个C++编译器(但不完全是这样)
  • 绘制系统中所有类的类图,看看它们是如何通信的
  • 用相同的类画另一个,看看他们应该如何交流
  • 重构代码以将第一个图转换为第二个图。(这可能既混乱又棘手)
  • 记住,所有添加的新代码都要使用C++代码
  • 如果您还有一些时间,请尝试逐个替换数据结构,以使用更标准化的STL或Boost

最新更新