博客 阅读时间 5 分钟

C++ 机制相对 C 语言的重构开销

分享本文
The refactoring overhead costs of the C++ mechanisms over C

几年前,Linus Torvalds 批评了 C++,他说:

"低效的抽象编程模型:两年之后你才发现某个抽象并不是很高效,但此时你所有的代码都依赖于围绕它构建的那些漂亮的对象模型,不重写整个应用就无法修复。"

许多 C++ 开发者并不认同这种观点。然而,我们必须承认,每个语言特性都有其代价,理解这种代价总比假定自己喜爱的编程语言完美无缺要好。

请记住,没有任何语言、技术或操作系统是完美的。不过,了解它的局限和缺点,可以帮助您更有效地使用它。

为此,我们将分析Git的源代码,发现一些设计上的事实。Git 是一个注重速度的分布式版本控制和源代码管理(SCM)系统。Git 最初由 Linus Torvalds 为 Linux 内核开发而设计和开发;此后已被许多其他项目采用。

让我们比较一下某些 C++ OOP 机制与其 C 语言等价物在重构开销上的差异。

模块化:命名空间 vs 目录

模块化是一种软件设计技术,它提高软件由独立部分组合而成的程度,使模块化代码更易于管理和维护。

我们可以用两种方式对项目进行模块化:

  • 物理方式:使用目录和文件。这种模块化形式由操作系统提供,可以应用于任何语言。
  • 逻辑方式:使用命名空间、组件和类。这种技术依赖于语言本身的能力。

用 C 开发时,可以使用目录来组织代码以隔离模块。下面是 Git 部分目录之间的依赖图。

然而,使用 C++ 而不是 C 时,我们可以使用命名空间来模块化代码;这些结构由语言提供。对于前面的图,我们可以用命名空间代替目录来模块化代码。

C++ 方式的优缺点:易于理解:逻辑方式更好,因为模块化由语言构件明确定义,只需阅读代码,我们就能知道某个代码元素存在于哪个模块中。

变更管理:好的设计通常需要多次迭代,使用物理方式时,设计变更的影响比逻辑方式要有限得多:事实上,我们只需将函数或变量从一个文件移到另一个文件,或将文件从一个目录移到另一个目录。而在 C++ 中,这样的变更可能影响大量代码,因为逻辑模块化是通过语言结构实现的,因此需要修改代码。

封装:类 vs 文件

在 C++ 中,封装是将数据和函数组合成一个称为类的单元的过程。使用封装方法,程序员无法直接访问数据。数据只能通过类内部的函数访问。

在 C 语言中,我们也可以实现封装,但采用的是类似模块化一节所述的物理方式:一个类可以是一个包含函数及其所用数据的文件,我们可以使用"static"关键字限制函数和变量的可访问性。

Git 使用这种技术来隐藏函数和变量;为了验证这一点,让我们搜索静态函数:

from m in Methods where m.IsStatic select m

矩形树图对于清晰概览受 CQLinq 查询影响的代码元素非常有用;蓝色矩形代表查询结果。

几乎所有函数都被声明为 static,使它们只在声明它们的翻译单元内可见;变量也是如此。

from f in Fields where f.IsStatic select f
易于理解:

使用 C++ 的封装机制可以提高代码清晰度,让各种关系更容易理解。

变更管理:如果我们必须更改变量或函数被封装的位置,对 C 来说可能非常容易,但对 C++ 来说可能影响大量代码。

多态 vs 选择惯用法

多态意味着代码、操作或对象可以在不同的上下文中表现出不同的行为。

这种技术在 C++ 项目中被广泛使用,但 C 语言呢?

在过程式语言中,选择通常使用"switch"、"if"甚至"goto"等关键字实现,但这种方式往往会增加代码的圈复杂度。

让我们在 Git 源代码中搜索复杂的函数。

尽管 Git 开发得很好,但仍有许多函数可以被认为是复杂的。这部分是由于大量使用了"if"、"switch"和"goto"等控制流语句。而使用 C++,我们可以利用多态来降低代码的复杂性。

易于理解:使用多态可以将特定行为隔离到一个类中;它提高了代码的可见性和内聚性。

变更管理:用多态添加另一种行为可能意味着要新增一个类;然而,使用选择惯用法时,您只需在 switch 语句下再添加一个 case。

继承 vs 组合

Git 主要使用结构体来定义由函数操作的数据。让我们搜索所有使用的结构体:

from t in Types where t.IsStructure select t

有趣的是,几乎所有数据都封装在结构体内。为了验证这一点,我们可以搜索所有不在结构体内的非常量公有基本类型变量:

from f in Fields where f.IsPublic && f.IsPrimitiveType
&& !f.IsStatic && !f.IsConst
select f

只有少量变量属于这种情况,这是 Git 设计的一个优点。

那么,如何扩展一个结构体呢?在 C 语言中我们可以使用组合,就像"remote"结构体那样——许多结构体都引用它。

然而,在 C++ 中我们还可以使用继承来扩展结构体;例如,known_remote 结构体可以继承自 remote。

易于理解:使用继承可以增进对数据的理解,但使用时必须小心;它只应用于"是一个"(Is-a)关系。

变更管理:继承意味着高耦合,所以任何变更都可能影响大量代码。

结论:

C++ 提供了更多编写整洁、结构良好代码的方式,但这是有代价的:变更和重构可能更加困难。

重构需要在修改之前理解现有代码。C 程序可能更难理解,但更容易修改。

我们该如何限制 C++ 中变更的影响呢?

限制变更影响的一个好方法是使用设计模式,特别是低耦合和高内聚原则,将变更隔离在特定区域内。正如之前文章中所解释的,Irrlicht 就是运用低耦合的一个好例子。

分享本文