第五章、实现

条款26、尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)

  1. 太快定义变量可能造成效率上的拖延。特别是当存在某些控制流未被执行时,提前定义的变量有可能导致本可以避免的构造和析构成本。
  2. 最好的方式是延后变量定义到它要被使用的前一刻,甚至是延后到可以直接给它实参为止。这样的话不仅可以避免构造非必要对象,还可以避免毫无意义的default构造行为。

条款27、尽量少做转型动作(Minimize casting)

  1. 过度使用转型可能导致代码变慢且难维护。
  2. C风格的转型语法:T(expression)或 (T)expression
  3. C++ new cast:const_cast( expression )static_cast( expression )dynamic_cast( expression )reinterpret_cast( expression )

这些转型操作符的作用如下:
const_cast( expression ):用来将对象的常量性移除(唯一有此功能的C++style转型操作符)。
static_cast( expression ):用来强迫隐式转换,常用于non-const对象转型为const对象、将void*指针转换成typed指针、将pointer-to-base转换成pointer-to-derived,以及上述多种转换的反向转换(但无法将const对象转换为non-const对象)。
dynamic_cast( expression ):用来执行“安全向下转型”,也就是用来决定某对象是否归属于继承体系中的某个类型。该转型无法由旧式转型语法完成,同时可能耗费重大运行时成本。
reinterpret_cast( expression ):意图执行低级转型,实际结果可能取决于编译器,故可移植效果不好。

  1. C++style转型的优点主要体现在:1、在代码中的辨识度高;2、将转型动作的目标窄化,便于编译器诊断出转型的错误运用。
  2. 对于单一对象而言,它可能拥有多个地址(如果有个偏移量在运行期被施行于derived*身上的话,则以“base”指针指向它时和例如以“derived”指针指向它时取得的地址不同)。
  3. C++代码应该尽量少的使用转型,如果转型是必要的,尽量将转型动作隐藏在某个函数内,这样可以方便客户的使用。

条款28、避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)

  1. references、指针、迭代器等可以取得实际对象的类型都是所谓的handles,而返回一个指向对象内部数据的handle会有降低封装性的风险。
  2. 避免返回handle指向对象内部,可以增加封装性,帮助const成员函数的行为像个const,以及将发生“虚吊号码牌”的可能性降低。

条款29、为“异常安全”而努力是值得的(Strive for exception-safe code)

  1. 当异常被抛出时,带有异常安全性的函数会有如下保证:1.不泄露任何资源;2.不会导致数据败坏。
  2. 异常安全函数可以提供以下三个保证之一:1.基本承诺;2.强烈保证;3.不抛掷保证。

基本承诺:如果异常被抛出,程序内的任何事物依然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有的对象都处于一种前后一致的状态。
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知,如果函数成功,就是完全成功,如果函数失败,程序会回复到调用函数之前的状态。
不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有的操作都提供nothrow保证。

  1. 从异常安全性的观点来看,nothrow函数很好,但是很难在C++领域中完全没有调用任何一个可能抛出异常的函数。所以对于大部分函数而言,抉择一般会落在基本保证和强烈保证之间。
  2. 对于copy and swap策略,它很典型的会导致强烈保证;不过需要注意的是,强烈保证并非对所有的函数都可实现或有现实意义。

copy and swap原则:为你打算修改的对象(原件)做一个副本,然后在这个副本上做一切必要的修改。若有任何修改动作抛出异常,原对象状态并未改变。待所有的修改全部成功后,再将原对象和修改过的副本在一个不抛出异常的操作中”置换“。
事实上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象。这种手法常被称为pimpl idiom

  1. copy and swap策略是对对象做出全有或者全无改变的一个很好的办法。
  2. 函数提供的“异常安全保证”通常最高只等于其所调用的各个函数中的“异常安全保证”最弱的那个函数。

条款30、透彻了解inline的里里外外(Understand the ins and outs of inlining)

  1. 将大多数inlining限制在小型、被频繁调用的函数身上,可以使日后的调试和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,同时使程序的速度提升机会最大化。
  2. 过度使用inline函数会使程序体积增大。即使拥有虚内存,inline造成的代码膨胀问题也会导致额外的换页行为,降低指令高速缓存装置的击中率和效率损失。
  3. inline只是对编译器的一个申请,并不是强制命令。这个申请可以隐喻提出,也可以明确声明。隐喻提出的方法是将函数定义在class声明内。明确声明的方法是在函数定义前加上inline关键字。
  4. inlining在绝大多数C++程序中是编译期行为。一个申请了inlining的函数是否真正inline,取决于你的建制环境,主要是指的编译器。
  5. 大部分编译期拒绝太过复杂的函数inlining行为(例如带有循环或者递归),所有对virtual函数的调用(除非是最平淡无奇的函数)也都会使inlining落空。
  6. inline函数无法随着程序库的升级而升级。如果inline函数发生了改变,那么所有用到该函数的程序都必须重新编译。

条款31、将文件间的编译依存关系降至最低(Minimize compilation dependencies between file)

  1. 当一个文件包含另一个文件时,在本文件和被包含文件中会存在一种编译依存关系。如果这两个头文件中的任何一个文件有任何改变,那么包含这些文件以及使用它们的文件都必须重新编译。
  2. 支持“编译依存性最小化”的一般构想是:相依于声明式,而非定义式。

常见的设计策略:

  1. 如果使用object references或者object pointers可以完成任何,就不要使用objects。你可以只靠一个声明式就定义出指向该类型的reference或者pointer;但是如果定义某类型的object,就需要用到该类型的定义式。
  2. 尽量以class声明式代替class定义式。比如当你声明一个函数而它用到了某个class时,你不需要该class的定义;纵使函数以pass-by-value的方式传参。但是当调用该函数时,需要让该class的定义式先曝光。
  3. 为声明式和定义式提供不同的头文件。
  1. 基于“编译依存性最小化”思想的两个常见手段是Handle classes和Interface classes。

基于pimpl idiom手法实现的很多class都属于Handle class。

  1. Handle class和Interface class解除了接口和实现之间的耦合关系,降低了文件间的编译依存性。
  2. 对于Handle class来说,成员函数必须通过implementation pointer取得对象数据。这会为每次的访问增加一层间接性。每一个对象消耗的内存数量也会增加implementation pointer的大小。implementation pointer还需要初始化并指向一个implementation object,所以会有动态内存分配带来的额外开销,以及异常的可能性。
  3. 对于Interface class来说,由于每个函数都是virtual,所以每次的函数调用都会有间接跳跃成本。此外,该class派生出的对象会多出用于保存vptr的内存。
  4. inline和Handle class/Interface class配合使用的效果较好。
  5. 在程序发展过程中使用Handle class/Interface class可以使实现码变化时对其客户带来的冲击最小化。而当它们导致速度/或大小差异过大时以至于class的耦合性与之相比不明显时,应以具象类替换它们。
  6. 程序库头文件应该是“完全且仅有声明式”的形式存在,不论是否涉及template。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值