将C源代码转换为C ++
你会如何将相当大的(> 300K),相当成熟的C代码转换为C ++?
CI所考虑的types被分割成大致对应于模块的文件(即,比典型的基于OO类的分解更小的粒度),使用内部链接来代替私人function和数据,以及用于公共function和数据的外部链接。 全局variables被广泛用于模块之间的通信。 有一个非常广泛的集成testing套件可用,但没有单位(即模块)级别的testing。
我想到一个总的策略:
- 在C ++的C子集中编译所有东西,并使其工作。
- 将模块转换为巨大的类,以便所有的交叉引用都以类名称作为范围,但将所有函数和数据作为静态成员,并使其工作。
- 通过适当的构造函数和初始化的交叉引用将巨大的类转换为实例; 用适当的间接访问replace静态成员访问; 并得到那个工作。
- 现在,将项目作为一个不适当的OO应用程序来处理,然后编写unit testing,在依赖关系易于处理的地方进行分解,然后将其分解成不同的类。 这里的目标是在每次转换时从一个工作程序转到另一个工作程序。
显然,这将是相当多的工作。 这种翻译有没有案例研究/战争故事? 替代策略? 其他有用的build议?
注1:该程序是一个编译器,可能数以百万计的其他程序依赖于其行为不变,所以批量重写几乎不是一个选项。
注2:来源近20岁,每年可能有30%的代码stream失(线路修改+增加/以前的总线)。 换句话说,它被大量维护和扩展。 因此,其中一个目标是提高可信度。
[为了这个问题,假定C ++的转换是强制性的,而把它留在C中不是一个选项。 增加这个条件的意思就是去掉“把它留在C中”的答案。]
在几个月前刚开始几乎同样的事情时(在一个十年前的商业项目中,最初用“C ++只是C语言和智能struct
”的哲学而写),我会build议你使用相同的策略用大象吃一口,一口咬一口。 🙂
尽可能把它分成几个阶段,可以对其他部分的影响最小。 正如Federico Ramponi所build议的那样,构build一个外观系统是一个好的开始 – 一旦所有东西都有一个C ++外观并通过它进行通信,就可以用公平的确定性来改变模块的内部,以至于它们不会影响到它们之外的任何东西。
我们已经有了一个部分的C ++接口系统(由于之前较小的重构工作),所以这种方法在我们的例子中并不困难。 一旦我们把所有事情都作为C ++对象进行通信(花了几周的时间,在一个完全独立的源代码分支上工作,并在批准时将主分支的所有更改集成在一起),很less我们不能编译完全我们离开之前的工作版本。
转换还没有完成 – 我们已经为临时版本暂停了两次(我们的目标是每隔几个星期就要发布一次),但是一切正常,没有客户抱怨任何问题。 我们的QA人员也只能find一个我记得的问题。 🙂
关于什么:
- 编译C ++的C子集中的所有内容,并使其工作
- 实现一组外立面保持C代码不变?
为什么“强制转换为C ++”? 你可以包装C代码,而不必把它转换成巨大的类,等等。
你的应用程序有很多人在工作,并且需要不被破坏。 如果您认真对待大规模转换为面向对象的风格,您需要的是大规模的转换工具来自动化工作。
其基本思想是将数据组指定为类,然后获取重构代码的工具,将数据移入类中,只将该数据的函数移动到这些类中,并修改对该数据的所有访问以调用类。
你可以做一个自动的预分析来形成统计集群来获得一些想法,但是你仍然需要一个应用程序感知工程师来决定应该分组哪些数据元素。
一个能够完成这项任务的工具是我们的DMS Software Reengineering Toolkit 。 DMS拥有强大的C语言分析器来读取您的代码,将C代码捕获为编译器抽象语法树(与传统编译器不同),可以计算整个300K SLOC中的stream量分析。 DMS有一个可以用作“后端”的C ++前端; 一个人写C语言转换为C ++语法。
在大型航空电子系统上进行重大的C ++重新devise任务,让我们了解了使用DMS进行这种活动的方式。 请参阅www.semdesigns.com/Products/DMS/DMSToolkit.html上的技术文章,特别是通过自动程序转换重新deviseC ++组件模型
这个过程不是为了模糊的心。 但是比任何人都会考虑手动重构大型应用程序已经不怕辛苦了。
是的,我和这个公司有关系,是我们的首席架构师。
我会通过C接口编写C ++类。 不触摸C代码将减less混乱的机会,并显着加快过程。
一旦你有了C ++接口, 那么将代码复制粘贴到类中是一件简单的任务。 正如你所提到的 – 在这个步骤中进行unit testing是至关重要的。
GCC目前正在从C中转换到C ++。他们开始时将所有东西都移到了C和C ++的通用子集中。 在他们这样做的时候,他们向GCC发出了警告,他们发现的所有东西都在-Wc++-compat
下find。 这应该让你在旅途的第一部分。
对于后面的部分,一旦你真正拥有了用C ++编译器编译的所有东西,我会专注于replace那些有惯用C ++对应的东西。 例如,如果您使用的是使用Cmacros定义的列表,地图,集合,位向量,哈希表等,那么将这些移动到C ++中可能会获得很多收获。 与OO类似,如果您已经在使用C OO习惯用法(如struct inheritence),并且C ++将为您的代码提供更高的清晰度和更好的types检查,那么您可能会发现其中的好处。
你的列表看起来不错,除了我会build议先检查一下testing套件,然后在做任何编码之前尽可能的把它弄得尽可能紧。
让我们抛出另一个愚蠢的想法:
- 在C ++的C子集中编译所有东西,并使其工作。
- 从一个模块开始,将它转换成一个巨大的类,然后在一个实例中,然后构build出一个C接口(与从那个开始的接口相同)。 让剩余的C代码与C接口一起工作。
- 根据需要重构,一次将OO子系统从C代码扩展到一个模块,并在C接口变得无用时丢弃部分C接口。
除了要开始的方式之外,可能还有两件事要考虑你想要关注的内容 ,以及你想要停止的地方 。
你声明有一个大的代码stream失,这可能是你的努力的关键。 我build议你select代码中需要大量维护的部分,成熟/稳定的部分显然工作得很好,所以最好把它们留在原来的位置,除了可能用于某些带外立面的窗户外。
你想停止的地方取决于想要转换为C ++的原因。 这本身就不是一个目标。 如果这是由于某些第三方依赖,请将您的工作重点放在该组件的接口上。
我所使用的软件是一个庞大的旧代码库,现在已经从C到C ++“转换”了。 我认为这是因为GUI被转换为Qt。 即使现在它仍然大部分看起来像一个C类程序。 打破由公共数据成员引起的依赖关系,并用程序怪兽方法将大型类重构成更小的方法和类从来没有真正起飞,我认为,原因如下:
- 没有必要改变正在工作的代码,不需要增强。 这样做会在不添加function的情况下引入新的错误,最终用户不理解;
- 做可靠的重构是非常非常困难的。 许多代码片断如此之大,也非常重要,所以人们几乎不敢碰它。 我们有一套相当广泛的functiontesting,但是很难获得足够的代码覆盖率信息。 因此,难以确定是否已经有足够的testing来检测重构期间的问题;
- 投资回报率难以确定。 最终用户不会从重构中受益,因此必须降低维护成本,这会增加最初的成本,因为通过重构,您可以在成熟(即相当无缺陷的代码)中引入新的错误。 而重构本身也将是昂贵的…
NB。 我想你知道“有效使用遗留代码”书吗?
你提到你的工具是一个编译器,而且:“实际上,多次调度中的模式匹配,而不仅仅是types匹配,会更好”。
你可能想看看maketea 。 它提供了AST的模式匹配,以及来自抽象语法的AST定义,游客,变形金刚等等。
如果您有一个小型或学术项目(比如说less于10,000行),重写可能是您最好的select。 不pipe你想要什么,你都可以将它分解,而且不会花费太多时间。
如果你有一个真实世界的应用程序,我build议把它编译为C ++(通常意味着主要修复函数原型等),然后重构和OO包装。 当然,我并不赞同代码需要被OO构造才能被接受的C ++代码的理念。 我会做一个逐件转换,重写和重构,因为你需要(function或合并unit testing)。
这是我会做的:
- 由于代码已经有20年的历史了,所以不要使用分析器/语法分析器,而要用新的lex / yacc / bison(或类似的)等基于C ++的代码replace它,更易于理解和维护。 如果你有一个BNF方便,也更快发展。
- 一旦对旧代码进行了改进,就开始将模块包装到类中。 用接口replace全局/共享variables。
- 现在你所拥有的将是C ++中的编译器(尽pipe如此)。
- 绘制系统中所有类的类图,并查看它们如何通信。
- 再用同样的课程画一个,看看他们应该如何沟通。
- 重构代码以将第一个图转换为第二个图。 (这可能是麻烦和棘手)
- 请记住为所有添加的新代码使用C ++代码。
- 如果还剩下一些时间,可尝试逐个更换数据结构,以使用更标准化的STL或Boost。