Effective C++ 改善程序与设计的55个做法,总结笔记(下)

前言

Effective C++ 改善程序与设计的55个做法,总结笔记(上):
https://blog.csdn.net/afei__/article/details/83478161

六、继承和面向对象设计

32. 确定你的 public 继承塑模出 is-a 关系

继承是 is-a 关系,指 “是一个”,即父类的每条属性和方法都应该适用于子类。

33. 避免遮掩继承而来的名称

  • 对于变量和函数,子类的名称会遮掩父类的名称,即使函数是重载的也不行。
  • 为了避免这个问题,可在子类中使用 using 声明或使用 转交函数(forwarding functions)。如:
class Base {
public:
    void fun1() {
        std::cout << "Base fun1" << std::endl;
    }
};
 
class Derived : public Base {
public:
    // 1. 使用 using 声明
    using Base::fun1;
    // 2. 使用转交函数
    void fun1() {
        Base::fun1();
    }
    // 同名函数会遮掩掉父类的 fun1()
    void fun1(int a) {
        std::cout << "Derived fun1: " << a << std::endl;
    }
};

34. 区分接口继承和实现继承

  • 纯虚函数(pure virtual)目的是让子类只继承函数接口,而不关心子类怎么去实现它。
  • 非纯虚函数(impure vitual)目的是让子类可选择的实现这个接口,或是使用父类的默认实现。
  • 成员函数(non-virtual)目的是让子类继承这个函数以及它的实现,而不去改变它(即所有子类都使用这个相同的实现)。

35. 考虑 virtual 函数以外的其它选择

当我们为了解决问题而寻找某个设计方法时,可以考虑一些 virtual 函数的替代方案,如:

  • 使用 non-virtual interface(NVI)手法,这是一种名为模版方法模式的设计模式,使用成员函数包裹虚函数。
  • 将虚函数替换为函数指针的成员变量。
  • 将虚函数替换为 tr1::function
  • 将继承体系内的虚函数替换为另一个继承体系内的虚函数(策略模式)。

36. 绝不重新定义继承而来的 non-virtual 函数

任何情况下都不该重新定义一个继承而来的 non-virtual 函数。

37. 绝不重新定义继承而来的缺省参数值

  • 如果是 non-virtual 函数,条款 36 已经介绍过不要重新定义了。
  • 如果是 viatual 函数,由于缺省参数都是编译期间静态绑定的,而 viatual 函数确是运行时动态绑定的,所以将会导致混乱。
  • 想要 virtual 函数表现出你想要的行为却有遇到麻烦,聪明的做法就是使用条款 35 里的替代方案。

38. 通过复合塑模出 has-a 或 “根据某物实现出”

  • 复合(也叫组合 composition)是 has-a 关系,指 “有一个”。我们将某种类型中包含其它类型的关系叫做复合。
  • 如果两个类之间并非严格的 is-a 关系,继承不再适用,我们应该选择复合的形式实现它们。

39. 明智而慎重地使用 private 继承

  • private 继承意味着父类所有非私有成员在子类中都是 private 的。这样就帮我们复用父类代码且防止父类接口曝光。
  • 但是私有继承意味着不再是 is-a 关系,而更像是 has-a 关系。我们总是可以通过复合的方式替代私有继承,并且更容易理解,所以无论什么时候,只要可以,我们还是应该选择复合。
  • 一种极端情况下,即我们有一个空白的父类,私有继承可以更小的占用空间。

40. 明智而慎重地使用多继承

  • 多继承比单继承复杂,而且多继承可能导致二义性,以及对 virtual 继承的需要。
  • 父类中存在数据的话,virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,应尽量不使用 virtual 继承。
  • 多继承适用的一种场景是:public 继承某个接口类并 private 继承某个协助实现的类。

七、模板与泛型编程

41. 了解隐式接口和编译期多态

  • class 和 template 都支持接口和多态。
  • 在 class 中接口是显式的,即我们有具体的函数签名。多态则通过 virtual 函数发生在运行期。
  • 在 template 中接口是隐式的,基于有效的表达式。多态则是通过 template 的具现化和函数重载解析发生在编译期。

42. 了解 typename 的双重含义

  • 声明 template 参数时,前缀关键字 class 和 template 可互换。如:
// 下面两种写法对编译器来说完全相同
template<class T> class Widget;
template<typename T> class Widget;
  • 请使用 template 标识嵌套从属类型名称,但不能在 base class list(基类列)或 member initialization list(成员初始列)内以它作为基类的修饰符。

这一点我们慢点说,首先解释 “使用 template 标识嵌套从属类型名称”,看下面这个例子:

template<typename T>
void print(const T& container) {
    // 这样是不行的
    // T::const_iterator iter(container.begin());
    // 必须明确告诉 C++ 这个 T::const_iterator 是个类型而不是变量
    typename T::const_iterator iter(container.begin());
}

也许我们初衷是获取到集合的迭代器,但是由于泛型 T 可能并不是一个集合,它里面没有一个叫做 const_iterator 类型的嵌套类,编译器也可以把 const_iterator 当作一个类的静态变量,所以这里会存在歧义。因此我们必须使用 typename 告诉 C++ 这是一个类型而不是静态变量。
typename 被用来标识一个嵌套从属类型名称,然而还有一个例外,即不能在基类列或成员初始列内以它作为基类的修饰符。如:

// Base 是一个基类,Nested 是基类的一个嵌套类
template<typename T>
class Derive
    : public Base<T>::Nested { // base class list 中不允许 typename
public:
    explicit Derive(int x)
    : Base<T>::Nested(x) // member initialization list 中不允许 typename
    {
        typename Base<T>::Nested temp; // 其余情况需要加上 typename
    }
};

43. 学习访问模板化基类中的名称

编译器往往会拒绝在模板化基类中寻找继承而来的名称,因为基类的模板化可能被特化而那个特化版本也许并不会提供某一接口。所以我们可以使用 “this->” 指定,或使用 “using” 告诉编译器假设它存在。

template<typename T>
class Derived : public Base {
public:
    // 方式1,告诉编译器 func 位于基类中
    using Base<T>::func();
     
    void test() {
        // 方式2,假设 func 将被继承
        this->func();
    }
};

44. 将与参数无关的代码抽离 templates

Templates 会生成多个 classes 和函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。

45. 运用成员函数模板接受所有兼容类型

  • 请使用成员函数模板生成 “可接受所有兼容类型” 的函数。即完成一些类型的指针向上转型(例如 Derive* -> Base*)。
  • 如果声明的成员模板是用于 “泛化 copy 构造” 或 “泛化 assignment 操作” ,你还需要声明正常的 copy 构造和 assignment 操作符。
// 子类 Y 的智能指针指向父类 T 时就不会出问题了
template<class T>
class shared_ptr {
public:
    // copy 构造函数
    shared_ptr(shared_ptr const& rhs);
 
     // 泛化 copy 构造函数
    template<class Y>
    shared_ptr(shared_ptr<Y> const& rhs);
 
     // copy assignment
    shared_ptr& operator=(shared_ptr const& rhs);
 
     // 泛化 copy assignment
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const& rhs);
};

46. 需要类型转换时请为模板定义非成员函数

为了让类型转换可能发生于所有实参上,我们需要一个非成员函数(条款 24)。在模板中,为了令这个函数被自动具现化,我们需要将它声明在类内部。为了在类内部声明非成员函数,唯一的办法是令它成为一个 friend。如:

template<typename T>
class Rational {
public:
    // 定义在class内的函数都暗自inline,对于复杂函数而言,我们可以令friend函数调用一个辅助函数避免
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
 
    Rational(int numerator = 0, int denominator = 1)
        : mNumerator(numerator), mDenominator(denominator) {}
    int numerator() const { return mNumerator; }
    int denominator() const { return mDenominator; }
private:
    int mNumerator; // 分子
    int mDenominator; // 分母
};
 
int main() {
    Rational<int> oneFourth(1, 4);
    Rational<int> result;
    result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
    cout << "numerator: " << result.numerator() << endl;
    cout << "denominator: " << result.denominator() << endl;
}

47. 请使用 traits classes(特性类)表现类型信息

  • Traits 不是 C++ 关键字或一个预先定义好的构件,而是一种技术,也是 C++ 程序员共同遵守的协议。它要求对于内置类型和用户自定义类型的表现必须一样好。Traits classes 使得 “类型相关信息” 在编译期可用,它们以 templates 和 “templates 特化” 完成实现。
  • 使用重载函数的形式,可以使 Traits classes 在编译期完成对类型的检测。

例子:
以 iterator_traits 为例介绍如何实现和使用 traits classes。STL 提供了很多的容器、迭代器和算法,其中的 advance 便是一个通用的算法,可以让一个迭代器移动给定距离:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // d < 0 就向后移动
STL 迭代器回顾:
struct input_iterator_tag {}; // 输入迭代器,只能向前移动
struct output_iterator_tag {}; // 输出迭代器,只能向前移动
struct forward_iterator_tag: public input_iterator_tag {}; // 稍强的是前向迭代器,可以多次读写它的当前位置
struct bidirectional_iterator_tag: public forward_iterator_tag {}; // 双向迭代器,支持前后移动
struct random_access_iterator_tag: public bidirectional_iterator_tag {}; // 随机访问迭代器,可以支持 +=, -= 等移动操作

回到 advance 上,它的实现取决于 Iter 类型:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
    // 针对 random access 迭代器使用这种方式实现
    if (iter is a random access iterator) {
        iter += d;
    } else {
        // 其它迭代器使用 ++ 或 -- 实现
        if (d >= 0) { while (d--) ++iter; }
        else { while (d++) --iter; }
    }
}

接下来就是怎么判断 Iter 的类型是否是 random access 迭代器了,也就是需要知道它的类型。这真是需要使用到 Traits classes 的地方。

实现 Traits classes:
用户自定义类型:

template<typename IterT>
struct iterator_traits {
    // 类型 IterT 的 iterator_category 就是用来标识迭代器的类别
    typedef typename IterT::iterator_category iterator_category;
};

指针类型:
指针本身就是可以支持随机访问(random access)的,所以我们对指针类型提供一个偏特化版本即可:

template<typename IterT> // template偏特化
struct iterator_traits<IterT*> { // 针对内置指针
    typedef random_access_iterator_tag iterator_category;
};

advance 实现:

不好的实现方式:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
        typeid(std::random_access_iterator_tag))
    // ...
}

IterTiterator_traits::iterator_category 都是可以在编译期间确定的,而 if 判断却要在运行期间核定,这样不仅浪费时间,也会导致代码膨胀。
建议做法是建立一组重载函数(doAdvance),接受不同的类型,原函数(advance)调用这些重载函数。

// 原函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    // 调用不同版本的重载函数
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
 
// 下面是一系列重载函数
// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
    iter += d;
}
 
// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
}
 
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
    if (d < 0 ) {
       throw std::out_of_range("Negative distance");
    }
    while (d--) ++iter;
}

48. 认识 template 元编程

  • 模板元编程(template metaprogramming TMP)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。上述 traits 就是 TMP 的应用。
  • TMP 可被用来生成 “基于政策选择组合” 的客户定制代码,而可用来避免生成对某些特殊类型不适合的代码。

八、定制 new 和 delete

49. 了解 new-handler 的行为

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • new (std::nothrow) 是一个颇为局限的工具,它只适用于内存分配,后续的构造函数调用还是可能抛出异常。
  • new-handler 函数通常需要完成以下事情:
  1. 让更多内存可以使用。既然内存不够那么我们就想办法弄到更大的内存。

  2. 安装另一个 new-handle。例如本 new-handler 无法取得更多内存,那么就找一个可以取得更多内存的 new-handler 替代自己(只要调用 set_new_handler 即可)。

  3. 卸除 new-handle。即将 null 传给 set_new_handler,然后 new 操作会在内存分配失败后抛出异常。

  4. 抛出 bad_alloc 异常。

  5. 不返回。通常调用 abort 或者 exit。

  6. 了解 new 和 delete 的合理替换时机
    有许多理由需要写个自定义 new 和 delete。如:

  7. 检测运行错误。

  8. 收集和统计 heap 使用信息。

  9. 改善性能。例如定制化的分配器来增加分配和归还速度,减少内存管理器带来的额外开销,弥补分配器中的非最佳对齐位等。

  10. 编写 new 和 delete 时需固守常规
    operator_new 应该内含一个无限循环,并在其中尝试分配内存,如果无法满足内存需求,就该调用 new-handle。它也应该有能力处理 0 bytes 申请,还应该能处理 “比正确大小更大的(错误)申请” 。
    operator_delete 应该在收到 null 指针时不做任何事。同样也应该能处理 “比正确大小更大的(错误)申请” 。

  11. 写了 placement new 也要写 placement delete
    当 operator new 接受的参数除了那个 size_t 外还有其它,这称之为 placement new。placement delete 同理。
    当你写一个 placement operator new 时,也务必写出对应的 placement operator delete。否则可能发生内存泄漏。
    当你声明 placement new 和 placement delete,请确定不要无意识地遮掩了它们的正常版本。

九、杂项讨论

53. 不要忽略编译器的警告

请严肃对待编译器的警告,努力在编译器最高警告级别下争取 “无警告”。

54. 让自己熟悉包括 TR1 在内的标准程序库

  • C++ 标准程序库主要由 STL,iostream,locales 组成,并包含 C99 标准程序库。
  • TR1 组件都在 std::tr1:: 命名空间下,以下是组件实例:
  1. 智能指针。
  2. tr1::function,常用于实现回调函数。
  3. tr1::bind,能够做 STL 绑定器 bind1st 和 bind2nd 所做的每一件事,而又更多。
  4. Hash tables,用来实现 sets,multisets,maps 和 multi-maps。
  5. 正则表达式。
  6. Tuples 变量组,这是标准程序库 pair 的升级,pair 只能持有两个对象,而 tr1::tuple 可持有任意个数对象。
  7. tr1::array,本质是个 STL 化的数组,即一个支持成员函数 begin 和 end 的数组。不过它大小固定,并不使用动态内存。
  8. tr1::mem_fn,这是一个语句上构造与成员函数指针(member function pointers)一致的东西。同样容纳并扩充了 C++98 的 mem_funmem_fun_ref 的能力。
  9. tr1::reference_wrapper,一个让引用的行为更像对象的设施。
  10. 随机数生成工具,它大大超越了 rand。
  11. 数学特殊函数,包括 Laguerre 多项式、Bessel 函数、完全椭圆积分,以及更多数学函数。
  12. C99 兼容扩充,这是一大堆函数和模版用来将许多新的 C99 程序库特性带入 C++。
  13. Type traits,一组 traits classes(条款 47),用以提供类型的编译期信息。
  14. tr1::result_of,这是一个用来推导函数调用的返回值类型的模版。

55. 让自己熟悉 Boost

  • Boost 是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的 C++ 程序库开发。
  • Boost 提供许多 TR1 组件的实现品,以及其它许多程序库。

你可能感兴趣的:(C/C++)