Effective C++ 学习笔记 第五章:实现

第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++
第二章见 Effective C++ 学习笔记 第二章:构造、析构、赋值运算
第三章见 Effective C++ 学习笔记 第三章:资源管理
第四章见 Effective C++ 学习笔记 第四章:设计与声明

文章目录

    • 条款 26:尽可能延后变量定义式的出现时间
      • 原书建议
    • 条款 27:尽量少做转型动作
      • 原书建议
    • 条款 28:避免返回handles 指向对象内部成分
      • 原书建议
    • 条款 29:为“异常安全”而努力是值得的 (重要)
      • 话题 1:不抛出异常保证
      • 话题 2:强烈保证
      • 话题 3:连带影响
      • 原书建议

条款 26:尽可能延后变量定义式的出现时间

Postpone variable definitions as long as possible.

尽可能的在需要使用变量(或指类型对象)时,再定义对象,尽可能避免无用的构造和析构操作。

如果构造对象之后,紧接着需要对对象进行赋值操作,更好的设计是直接调用有参数的构造函数直接完成。如:

std::string enc;  // 调用默认构造函数
enc = pas;        // 再调用赋值运算符重载函数
// 以上不如直接替换为以下:
std::string enc(pas);  // 调用 copy 构造函数

如果一个对象是循环内变量,若满足以下两个条件:

  1. 能确定构造、析构操作比赋值操作代价低;
  2. 需要高度性能敏感的代码;

则将循环内变量外提,在循环之前定义;否则,建议在循环内定义对象。
原因是,前者的代价是:1 次构造 + 1 次析构 + n 次赋值;后者的代价是:n 次构造 + n 次赋值。(n 为循环次数)
建议使用后者的原因是,尽可能的控制对象的作用域。

原书建议

  • 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

条款 27:尽量少做转型动作

Minimize casting.

传统的 C 转型方式有两种:

(T)expression    // 将 expression 转型成 T 类型
T(expression)    // 同理,函数样式转型

C++ 中提供了新型转型:

const_cast<T> (expression)    // const 属性去除转型,C 转型没有这个功能
dynamic_cast<T> (expression)  // 安全继承向下转型(base 到 derive),谨慎用
reinterpret_cast<T> (expression)   // 低级转型,比如将指针类型转型成整形,不常用
static_cast<T> (expression)  // 比较常用,强制转换,但不支持 const_cast 的效果

建议使用新型转型,原因:

  1. 容易辨识,也容易自动化查找(如 grep);
  2. 功能效果细化,容易控制,不易出错;

谨慎使用转型动作,尽可能用其他设计来替代转型动作。
dynamic_cast 转型可能会非常慢,尽量避免使用,可使用多态的方案替代。
或者将转型动作藏起来。

原书建议

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果需要转型,试着使用其他无需转型的替代方案。
  • 如果必须要用转型,试着将其藏在函数内部,避免客户在他们自己的代码中去使用转型。
  • 宁可使用 C++ 风格的新式转型,不要使用旧式转型。

条款 28:避免返回handles 指向对象内部成分

Avoid returnning “handles” to object internals.

本条款中所提的 handles 是指引用、指针等类型,可以间接访问到对象内部数据。

举例说,如果我们设计了一个类内公有成员函数,返回一个类私有成员的引用:

// 假设类 Rec 中有指针私有数据 pData,指向 Point 类型的对象 point
Point& Rec::getPoint() const { return pData->point; }

我们这样的设计,因为加了 const,本身是不希望 point 数据被修改,虽然 getPoint 本身无法修改 point 的值了,但其返回 point 数据的引用,用户可以通过引用来操作 point,所以,本质上并没有保证封装性。
成员变量的封装性最多只等于返回其 handles 的函数的访问级别。

我们可以通过修改为以下形式来避免问题:

const Point& Rec::getPoint() const { return pData->point; }

返回 handles 还会带来另一个潜在的问题,这便是如果返回的 handles 生存期长于 它所指向的对象时,导致 handles 被挂起(dangling)的问题。比如返回指针指向的对象仅仅是函数内部拷贝的临时对象的情况。

有时的设计必须这么做,那便不得不做,但尽可能避免。

原书建议

  • 避免返回 handles (包括引用、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生 dangling handles 的可能性将至最低。

条款 29:为“异常安全”而努力是值得的 (重要)

Strive for exception-safe code.

当异常抛出时,具有异常安全性的函数应该具有:

  • 不泄露任何资源。
  • 不允许产生错误的数据。

如下例子:

class M {
public:
    void changeB(std::istream& img);
private:
    Mutex mutex;         // 图像改变时需要互斥
    Image* bgImg;        // 保存当前图像
    int imageChanges;    // 用来记录图像改变几次
};
void M::changeB(std::istream& img) {
    lock(&mulex);        // 获得互斥器
    delete bgImg;        // 删除旧的图像
    ++imageChanges;      // 图像更改次数增加
    bgImg = new Image(img);    // 更换新的图像
    unlock(&mutex);      // 释放互斥器
}

我们习惯会这么写代码,但是,这不是异常安全性的函数。如果 new Image(img) 时抛出异常,则互斥锁无法释放(违反:不泄露任何资源),而且imageChanges 实际已经改变,且bgImg 将为空指针(均违反:不允许产生错误数据)。

异常安全性的函数提供三种保证之一:

  • 基本承诺:这是最基本的保证,保证发生异常后,程序内的状态依然有序,没有数据和对象被破坏。对于上例来说,便是能保证图像改变次数正常,图像指针不会为空。
  • 强烈保证:更高级的保证,保证发生异常后,程序状态不变,也就是一旦发生了异常,程序会完全退回到发生异常之前的状态。对于上例来说,就像从来没调用过 changeB 函数一样。
  • 不抛出异常保证,这是最佳保证,承诺不抛出异常,也就承诺了程序一定按正常的控制流执行,也就是一定能执行成功。

话题 1:不抛出异常保证

通常来说比较困难,只要使用了自定义的类型,比如容器类,就难免会遇到内存不足的异常(bad_alloc)。如果真的可以做到是最好。

话题 2:强烈保证

对于上例来说,强烈保证可以通过资源对象和智能指针来管理 bgImg 成员,在之前的条款中大概都提到过。

class M {
    std::shared_ptr<Image> bgImg;
    ...
};
void M::changeB(std::istream& img) {
    Lock ml(&mutex);        // 将 mutex 交给资源对象 ml 管理
    bgImg.reset(new Image(img));    // 这里的 bgImg 是智能指针
    ++imageChanges;
}

ml 对象来管理 mutex,将不再需要 unlock 操作,这个操作实际放在了 Lock 类的析构函数中,在退出 changeB 函数时会自动调用。而 bgImg 的使用,可以保证在 new Image(img) 完全正常的情况下,才修改 bgImg,如果 new Image(img) 出现异常,则不会进入 reset 函数内部,进而能够保证原始图像不变。

另一种非常巧妙的办法是 copy and swap 方法。也就是,先复制一个副本,对副本做好操作,再用 swap 将副本和原始对象做交换,我们还记得,swap 函数是需要保证不抛出异常的函数。

struct PM {         // 我们将所有的类资源拿出来单独做一个结构体
    std::shared_ptr<Image> bgImg;
    int imageChanges;
};
class M {
private:
    Mutex mutex;
    std::shared_ptr<PM> pIm;
};
void M::changeB(std::istream& img) {
    using std::swap;        // 条款 25 中描述很详细
    Lock ml(&mutex);
    std::shared_ptr<PM> pNew(new PM(*pIm));    // copy 操作,获取副本 pNew
    pNew->bgImg.reset(new Image(img));         // 修改副本
    ++pNew->imageChanges;                      // 修改副本
    swap(pIm, pNew);                           // 完成交换
} // 最后还会自动释放 mutex,new 出来的资源由 pNew 交给 pIm,不用担心 pNew 析构

话题 3:连带影响

有些时候,我们很难做到强烈保证,这是因为连带影响。意思是,虽然我们自己的代码做到了高保证,但是我们的代码调用了一些其他代码,那些代码却没有做到高保证,那么实际上我们的代码也不是高保证。
所谓之,异常安全性级别,通常等价于那段代码中最差的安全性保证的级别。

当然,还有另一个原因是,强烈保证通常需要提供更多的资源来完成,比如为了 copy and swap,我们需要额外的一个资源副本,如果在一些场合下不适合提供这种条件,则我们只能做到基本保证。

将异常安全性做的越好的趋势,是一种期许,并没有说异常安全性差就是错,只是说那样做不够好。

原书建议

  • 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构被破坏,有三种可能的保证:基本保证、强烈保证和不抛出异常。
  • 强烈保证通常能够使用 copy and swap 方式实现,但有些时候,这样做有点困难,所以并不强求。
  • 函数提供的异常安全性保证级别等于它所调用的各个函数代码的异常安全性保证级别中的最差者。

你可能感兴趣的:(C++,学习笔记)