如何最好地从模板混乱切换到干净的类架构(C ++)?

时间:2008-11-27 15:33:37

标签: c++ templates simplify

假设一个较大的模板库包含大约100个文件,其中包含大约100个模板,总共超过200,000行代码。一些模板使用多重继承来使库本身的使用变得相当简单(即从一些基本模板继承并且只需要实现某些业务规则)。

所有存在的(长达数年),“有效”并用于项目。

但是,使用该库编译项目会占用越来越多的时间,并且需要花费相当长的时间来查找某些错误的来源。修复通常会导致意外的副作用或非常困难,因为某些相互依赖的模板需要更改。由于功能的庞大,测试几乎是不可能的。

现在,我真的想简化架构以使用更少的模板和更专业的小类。

有没有经过验证的方法可以完成这项任务?什么是一个好的开始?

8 个答案:

答案 0 :(得分:13)

我不确定我是怎么看/为什么模板是问题,为什么简单的非模板类会是一个改进。这不仅仅意味着更多类,更少的类型安全性以及更大的漏洞潜力吗?

我可以理解简化架构,重构和删除各种类和模板之间的依赖关系,但自动假设“更少的模板将使架构更好”是有缺陷的。

我会说模板可能允许你构建一个比没有它们时更清晰的架构。仅仅因为你可以使完全独立的单独的类。如果没有模板,调用另一个类的类函数必须事先知道该类或它继承的接口。使用模板,这种耦合不是必需的。

删除模板只会导致更多依赖项,而不是更少。 增加的模板类型安全性可用于在编译时检测大量错误(为此目的使用static_assert自由地使用代码)

当然,在某些情况下,增加的编译时间可能是避免模板的正当理由,如果您只有一群习惯于用“传统”OOP术语思考的Java程序员,模板可能会混淆他们,这可能是避免模板的另一个有效理由。

但从架构的角度来看,我认为避免使用模板是朝着错误方向迈出的一步。

重新构建应用程序,当然,这听起来像是需要的。但是,不要因为应用程序的原始版本滥用它而丢弃一个最有用的工具来生成可扩展和健壮的代码。特别是如果您已经关注代码量,删除模板很可能会导致更多代码行。

答案 1 :(得分:7)

你需要自动化测试,十年后你的成功者会遇到同样的问题,他可以重构代码(可能是为了增加更多模板,因为他认为这会简化库的使用)并且知道它仍然符合所有的测试案例。同样,任何小错误修复的副作用都将立即可见(假设您的测试用例很好)。

除此之外,“划分和征服”

答案 2 :(得分:3)

编写单元测试。

新代码必须与旧代码相同。

这至少是一个提示。

编辑:

如果您弃用已使用新功能替换的旧代码 可以一点一点地逐步转移到新代码。

答案 3 :(得分:2)

嗯,问题在于模板思维方式与面向对象继承的方式有很大不同。除了“重新设计整个事物并从头开始”之外,很难回答任何问题。

当然,对于特定情况可能有一种简单的方法。如果不了解更多关于你拥有的东西,我们无法分辨。

模板解决方案难以维护这一事实无论如何都表明设计不佳。

答案 4 :(得分:2)

有些观点(但请注意:这些不是邪恶。如果您想要更改为非模板代码,这可能会有所帮助):


查找您的静态接口。模板在哪里取决于存在哪些功能?他们在哪里需要typedef?

将公共部分放在抽象基类中。一个很好的例子就是当你碰巧偶然发现CRTP成语时。您可以将其替换为具有虚函数的抽象基类。

查找整数列表。如果您发现代码使用list<1, 3, 3, 1, 3>之类的整数列表,则可以使用std::vector替换它们,如果使用它们的所有代码都可以使用运行时值而不是常量表达式。

查找类型特征。有很多代码涉及检查是否存在某些typedef,或者是否存在典型模板化代码中的某些方法。抽象基类通过使用纯虚方法和将typedef继承到基类来解决这两个问题。通常,只需触发typedef来触发像 SFINAE 这样的丑陋功能,这也是多余的。

查找表达式模板。如果你的代码使用表达式模板来避免创建临时代码,你将不得不消除它们并使用传统的方式将临时数据传递给相关的运算符。

查找功能对象。如果您发现您的代码使用了函数对象,您可以将它们更改为使用抽象基类,并使用void run();来调用它们(或者如果您想继续使用operator(),那么更好!也可以是虚拟的。)

答案 5 :(得分:1)

据我了解,您最关心的是构建时间和库的可维护性吗?

首先,不要试图一次性“修复”。

其次,了解你的修复方法。模板复杂性经常出于某种原因,例如,强制执行某些使用,并使编译器帮助您不犯错误。这个原因有时候可能会被采用,但抛出100行,因为“没有人真正知道他们做了什么”不应该掉以轻心。我在这里建议的所有内容都可以引入非常讨厌的错误,你已经被警告了。

第三,首先考虑更便宜的修复:例如更快的机器或分布式构建工具。至少,扔掉电路板将占用的所有RAM,并丢弃旧磁盘。它确实有所不同。一个用于操作系统的驱动器,一个用于构建的驱动器是便宜的勒芒RAID。

图书馆有详细记录吗?这是你制作它的最好机会查看doxygen等工具,帮助你创建这样的文档。

所有考虑过?好的,现在有一些关于构建时间的建议;)


理解C ++ build model :每个.cpp都是单独编译的。这意味着许多.cpp文件有很多header = huge build。但是,这并不是建议将所有内容放入一个.cpp文件中!但是,可以极大地加速构建的一个技巧(!)是创建一个包含一堆.cpp文件的.cpp文件,并且只将该“master”文件提供给编译器。但是,你不能盲目地这样做 - 你需要了解这可能引入的错误类型。

如果您还没有,可以远程进入单独的构建计算机。你将不得不做很多几乎完整的构建来检查你是否破坏了一些包含。您将希望在另一台机器上运行它,这不会阻止您处理其他事情。从长远来看,无论如何,你都需要它来进行日常集成;)

使用预编译标题。 (使用快速机器可以更好地扩展,见上文)

检查标题包含政策。虽然每个文件都应该是“独立的”(即包括其他人需要包含的所有内容),但不要自由包含。不幸的是,我还没有找到一个工具来查找不必要的#incldue语句,但是花一些时间删除“hotspot”文件中未使用的标题可能会有所帮助。

为您使用的模板创建并使用前向声明。通常,您可以在许多地方使用转发声明来添加标头,并且仅在几个特定的​​标头中使用完整标头。这可以极大地帮助编译时间。检查<iosfwd>标题标准库如何为i / o流执行此操作。

少数类型模板的重载:如果你有一个复杂的函数模板,它只适用于这样的极少数类型:

// .h
template <typename FLOAT> // float or double only
FLOAT CalcIt(int len, FLOAT * values) { ... }

您可以在标题中声明重载,并将模板移动到正文:

// .h
float CalcIt(int len, float * values);
double CalcIt(int len, double * values);

// .cpp
template <typename FLOAT> // float or double only
FLOAT CalcItT(int len, FLOAT * values) { ... }

float CalcIt(int len, float * values) { return CalcItT(len, values); }
double CalcIt(int len, double * values) { return CalcItT(len, values); }

这会将冗长的模板移动到单个编译单元 不幸的是,这仅限于课程。

检查the PIMPL idiom是否可以将代码从标头移动到.cpp文件中。

隐藏的一般规则是将您的库的界面与实现分开。使用注释,detail namesapces和单独的.impl.h标题来精神上和物理上隔离外部应该知道的内容和完成方式。这暴露了你的库的真正价值(它实际上是否封装了复杂性?),并让你有机会首先替换“简单目标”。

<小时/> 更具体的建议 - 以及给定的有用性 - 在很大程度上取决于实际的库。

祝你好运!

答案 6 :(得分:0)

如上所述,单元测试是一个好主意。实际上,不是通过引入可能会出现问题的“简单”更改来破坏您的代码,而是专注于创建一组测试,并修复不符合测试的问题。当bug被发现时,有一个活动来更新测试。

除此之外,如果可能,我建议升级您的工具,以帮助调试与模板相关的问题。

答案 7 :(得分:0)

我经常遇到庞大的遗留模板,需要大量的时间和内存才能实例化,但并不需要。在这些情况下,减少脂肪的最简单方法是获取所有不依赖于任何模板参数的代码,并将其隐藏在普通翻译单元中定义的单独函数中。当此代码必须稍微修改或文档更改时,这也会产生触发较少重新编译的积极副作用。这听起来相当明显,但是人们编写类模板的频率并且认为必须在标题中定义它所做的一切,而不仅仅是需要模板化信息的代码,这实在令人惊讶。

您可能想要考虑的另一件事是通过使模板“mixin”样式而不是多重继承的聚合来清理继承层次结构的频率。通过使其中一个模板参数成为它应该派生的基类的名称(boost::enable_shared_from_this的工作方式),查看可以获得多少个位置。当然,这通常只有在构造函数不带参数的情况下才能正常工作,因为您不必担心正确初始化任何内容。