五. 实现
26. 尽可能延后变量定义式的出现时间
请注意:
尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率。
解释:
-
增加程序的清晰度:这样可以让读者在第一时间内更好地理解变量的用途。变量定义在第一次使用的地方,可以清晰地看到其类型和初始值。如果变量在几页代码之前就已被定义,那么读者可能需要花费更多的时间在代码文件中寻找其定义和初次使用的地方,这对代码的阅读是不友好的。
-
改善程序效率:如果变量在定义的时候就初始化,那么可能会对性能产生影响,因为这实际上创建了变量并分配了内存,而在很多情况下,这都可能是不必要的。与之相反,将变量的定义和初始化处理延后到真正需要它的地方,这可以避免不必要的计算和内存分配,从而可以提高程序效率。
-
避免不必要的副作用:许多时候,对象的构造函数可能会产生副作用。在对象真正需要之前,其构造函数可能产生一些不必要的副作用。因此,将变量的定义延后,直到真正需要这个对象,可以帮助我们避免这些可能的问题。
综上,尽可能地延后变量定义式的出现,可以使代码更容易阅读和理解,也有助于提高代码质量和执行效率。
27. 尽量少做转型动作
请注意:
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必须的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。
- 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
解释:
在编程中,类型转换的滥用可能导致代码难以理解和维护,而且往往可以通过改进设计来避免。这是因为转型往往会破坏类型安全,增加运行时错误的机会。
在某些情况下,如果转型确实是必要的,有几种方式可以减轻其可能带来的负面影响:
-
封装转型操作:如果能够将转型隐藏在函数或方法中,那么调用者就无需在他们自己的代码中进行转型。这有助于使代码保持清晰和一致。
-
使用C++风格的转型:C++风格的转型(如static_cast、dynamic_cast、const_cast和reinterpret_cast)比C风格的转型(如(int) x或者int(x))更具可读性。它们明确指示了转型的方式和意图,使转型更容易被发现和理解,从而降低代码错误的概率。
-
避免使用dynamic_cast:dynamic_cast在运行时检查转换的有效性,这可能会产生较大的性能开销。如果能够改变设计以使用其他类型的转型,或者完全避免转型,那么代码可能会运行得更快。
-
提供无需转型的替代设计:如果设计需要频繁转型,这可能表明设计可以进一步改进。试图找到无需转型操作的设计方案。
总的来说,应该尽量避免使用转型,如果无法避免,则应选择最安全并易于理解的方式进行。
28. 避免返回handles指向对象内部成分
请注意:
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。
解释:
返回指向对象内部元素的引用、指针或迭代器实质上是在破坏封装,使得对象的内部结构暴露给外部。这样做可能会带来几个问题:
-
破坏封装:如果一个类的实现细节(即它的内部结构)被暴露给类的用户,那么类的用户可能就会依赖于那些细节。这样一来,如果这些实现细节在将来发生改变,那么使用该类的代码也就必须进行改动。
-
破坏const成员函数:如果一个const成员函数返回一个指向其数据成员的引用或者指针,那么即使这个引用或指针被声明为const,编译器也只能阻止通过这个引用或者指针修改数据成员,而不能阻止通过将其强制转换为非const引用或者指针来修改数据成员。
-
更高的"悬挂引用"风险:如果返回的引用、指针或迭代器是指向对象的局部变量或临时变量,那么这个引用、指针或迭代器可能就会变成一个"悬挂引用",也就是说,它指向的对象可能在它被使用之前就已经被销毁了。
所以,为了保证良好的封装性,并保护数据的完整性,避免返回指向对象内部的handles是个好的做法。如果需要返回对象的状态或者内部数据,可以考虑返回这些数据的副本,或者提供一些获取和设置这些数据的public成员函数。
29. 为“异常安全”而努力是值得的
请记住:
- 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
解释:
在编程中,我们确实把异常安全的函数分为三类:基本保证、强烈保证和不抛异常保证。这三类保证分别对应了不同程度的异常安全性。
-
基本保证:这意味着函数抛出异常后,程序的内部状态仍然保持一致,不会有内存泄漏或者数据结构被破坏的情况,但特定的操作可能未能完成。
-
强烈保证:这意味着函数抛出异常后,程序回到其调用函数之前的状态,也就是说,如果操作不能完成,那么就像根本没有进行过操作一样。这种保证通常通过事务(transaction)的概念进行实现,即将可能失败的操作进行拷贝和交换,确保在原子操作中完成。
-
不抛异常保证:这是最高级别的保证,意味着函数保证不抛出任何异常。
你提到的“函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者”,这个说法也是正确的。这是因为,如果一个函数f调用了另一个提供了较弱异常安全保证的函数g,那么,f的异常安全性就会受到g的影响。
因此,在设计和实现时,应当尽可能使得函数在遇到异常时也能保证安全,以防数据丢失或程序崩溃。同时,也要理解和注意函数间异常安全级别的影响关系。
30. 透彻了解inlining的里里外外
请记住:
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为function templates出现在头文件,就将他们声明为inline
解释:
你的理解是正确的。内联函数(inlining)是一种优化技术,通过把函数调用替换为函数体的内容来消除函数调用的开销,提高程序运行速度。然而,滥用内联函数会导致一些问题:
-
代码膨胀:过度的内联可能导致代码膨胀,因为函数体会被复制到每个调用它的地方。如果一个大型函数被频繁调用并且它被声明为内联的,那么这会导致最终的程序体积较大。
-
调试和更新的困难:内联函数的改变需要重新编译所有使用到它的代码,这会使得调试和更新过程复杂并且增加了维护的工作量。
-
性能没有明显提升:函数的内联并不一定能带来预期的性能提升。对于大型函数,由于代码膨胀导致的缓存未命中(cache miss)可能抵消了由于去除函数调用而带来的性能提升。所以,相比较大型函数,较小的、频繁调用的函数更适合声明为内联。
关于函数模板(function templates),由于它们通常定义在头文件中,编译器可能会自动将它们视为内联,即使你没有明确声明。然而,不应该仅仅因为函数模板在头文件中就设为内联,因为这可能导致之前提到的代码膨胀和维护问题。
总的来说,我们应当谨慎地使用内联,在明确知道它会带来性能提升的情况下,才将函数设置为内联。
31. 将文件间的编译依存关系降至最低
请记住:
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handless classes 和 Interface classes
- 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。
解释:
最小化编译依赖性。事实上,这是一种良好编程习惯,可以大大改善代码的维护性和可扩展性。
当我们说“依赖于声明,而不是依赖于定义”时,我们的意思是,我们应该把对特定代码块的依赖性限制在它的接口(即它的声明)上,而不是它的实现(即它的定义)上。
这样做的好处是,让我们能够更改或优化一个类或函数的实现,而不影响任何依赖于它的代码。这也解耦了我们的代码,使得每个部分更容易独立测试和修改。
Handle classes和Interface classes,都能有效地实现这个原则。
-
Handle classes(又叫做Pimpl,pointer to implementation):这种技术隐藏了类的实现细节,只在头文件中暴露一个接口。这样,即使类的实现发生了改变,也不会影响到使用这个类的其他代码。
-
Interface classes:这个方法使用纯虚函数定义一个接口,然后通过不同的子类来提供不同的实现。这样,客户端代码只依赖于接口,并不关心具体的实现。
无论你是在使用普通类还是模板,遵循“最小化编译依赖性”的原则,你的头文件应该只包含完成的声明,而不是定义。这样可以减少不必要的编译依赖,使代码更容易维护。