"> Effective C++读书笔记(下) | Stillwtm's Blog
0%

Effective C++读书笔记(下)

本文是笔者阅读Effective C++第三版时的读书笔记,主要内容包括:

  • 对书上内容的概括提取
  • 笔者自己不清楚的小知识点的提取
  • 对书中每个条款的后的“请记住”部分的(基本上是)原文摘录

这种灰色部分一般是笔者自己的一些注释

原书一共九章,本文(下篇)记录第七至九章的笔记

7. 模板与泛型编程

条款41:了解隐式接口和编译期多态

classes和 templates都支持接口和多态

对 classes而言接口是显式的,以函数签名为中心;多态则是通过 virtual函数发生于运行期

对 template参数而言,接口是隐式的,基于有效表达式;多态则是通过 template具现化和函数重载解析发生于编译期


条款42:了解typename的双重意义
  • 应该再嵌套从属类型名称之前加上typename消除歧义
    • 例外是,base class list和成员初值列中不应该加typename

声明 template参数时,前缀关键字 typename和 class可以互换

请使用关键字 typename标识嵌套从属类型名称;但不得在基类列或成员初值列内以它作为 base class修饰符


条款43:学习处理模板化基类内的名称
  • 在继承模板类时,C++编译器不会进入base class查找某个名称,因为模板基类有可能被特化从而失去那个东西
  • 若要让编译器看到模板类中的名称(即对编译器承诺基类的每一种特化都支持该接口),有三种方法:

    • 在调用基类函数之前加上this->
    • 使用using声明式
    • 直接指出被调用的函数位于基类中(通过作用域限定符)

      • 这种方法不是很好,因为它会关闭virtual绑定行为
      1
      2
      3
      4
      5
      6
      this->func();  // 方法一

      using BaseClass<T>::func(); // 方法二
      func();

      BaseClass<T>::func(); // 方法三
  • 编译器诊断的时间可能发生在早期(解析derived class template的定义式时),也可能发生在晚期(具现化该template时);C++的政策是宁愿较早诊断


可在 derived class templates内通过this->指涉 base class templates内的成员名称,或藉由一个明白写出的 “base class资格修饰符 ”完成


条款44:将与参数无关的代码抽离templates
  • 使用非类型参数的模板可以藉由编译期常量的广传达到最优化;而使用函数参数的版本则可能减少可执行文件的大小,也就降低程序的working set大小,并强化程序高速缓存区的引用集中化(locality of reference)

    • 欲知哪种影响占据主要地位,应该进行实验

    working set指对一个在“虚内存环境”下执行的进程而言,所使用的那一组内存页(pages)

  • 某些连接器会合并完全相同的函数实现码,而某些不会,后者意味着实际上相同的参数也可能会具现出两个版本(例如许多平台上intlong二进制表述实际相同)

    • 由于指针类型的二进制表述完全相同,所以当指针作为模板参数时,实际上应该让他们调用另一个操作void*的函数作为底层实现,以此避免代码膨胀

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

因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或 class成员变量替换 template参数

因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型(instantiation types)共享实现码


条款45:运用成员函数模板接受所有兼容类型
  • 使智能指针也支持隐式转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typename T>
    class SmartPtr {
    public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)
    : heldPtr(other.get()) { ... } // 这里利用heldPtr的赋值提供了转换的约束
    T* get() const { return heldPtr; }
    ...
    private:
    T* heldPtr;
    }
  • 除了拷贝构造,也可以类似地实现赋值操作符等,具体见书P221

  • 在类内声明泛化拷贝构造并不会阻止编译器自动生成普通的拷贝构造,所以应同时声明泛化的和“正常的”两种版本

请使用 member function templates生成 “可接受所有兼容类型 ”的函数

如果你声明 member function templates用于 “泛化 copy构造 ”或 “泛化 assignment操作 ”,你还是需要声明正常的 copy构造函数和 copy assignment操作符


条款46:需要类型转换时请为模板定义非成员函数
  • template实参推导过程中从不将隐式类型转换函数纳入考虑(也就是说,应该先推定参数类型后,才可能进行隐式类型转换以符合该类型)
  • 当模板函数的参数需要支持某个模板类的隐式类型转换时,可以将该函数定义为该模板类的友元,这样具现模板类的时候同样会具现这个友元函数,从而可以免去模板参数类型推定

当我们编写一个 class template,而它所提供的 “与此 template相关的 ”函数支持 “所有参数的隐式类型转换 ”时,请将那些函数定义为 class template的 friend函数


条款47:请使用traits classes表现类型信息
  • 设计并实现一个traits class来表现类型信息:

    traits通常被实现为structs,却往往被称为trait classes

    • 确认你希望将来可取得的类型相关信息,例如迭代器的种类
    • 为该信息选择一个名称,例如iteration_category
    • 提供一个template和一组特化版本,内含希望支持的类型相关信息,一般会使用一个typedef
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    struct input_iteration_tag {};
    struct output_iteration_tag {};
    struct forward_iteration_tag: public input_iteration_tag {};
    struct bidirectional_iteration_tag: public forward_iteration_tag {};
    struct random_access_iteration_tag: public bidirectional_iteration_tag {};

    class deque {
    public:
    class iterator {
    typedef random_access_iteration_tag iterator_category;
    ...
    }
    }

    template<typename IterT>
    struct iterator_traits {
    typedef typename IterT::iterator_category iterator_category;
    ...
    }

    // 针对指针的偏特化,因为traits应该对用户定义类型和内置类型表现同样好
    template<typename IterT>
    struct iterator_traits<T*> {
    // 指针应该和随机访问迭代器类似
    typedef random_access_iteration_tag iterator_category;
    ...
    }
  • 使用一个trait class来在编译期确定类型

    下面的写法免去使用typeid()在运行时判断类型,而是利用函数重载,这也看出为什么上面的实现使用struct来描述类型而非enum

    • 建立一组重载函数,彼此间的差异只在于各自的traits参数
    • 建立一个控制函数,它调用上述那些函数并传递traits classes所提供的信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d,
    random_access_iteration_tag) {
    iter += d;
    }

    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d,
    bidirectional_iteration_tag) {
    ...
    }

    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d,
    input_iteration_tag) { // 这份实现同样适用于forward,这是继承关系的红利
    ...
    }

    template<typename IterT, typename DistT>
    void advance(IterT& iter, DistT d) {
    doAdvance(iter, d,
    typename iterator_traits<IterT>::iterator_category())
    }

Traits classes使得 “类型相关信息 “在编译期可用。他们以templates和 “templates特化 “完成实现

整合重载技术后,traits classes有可能在编译期对类型执行 if-else测试


条款48:认识template元编程
  • TMP是图灵完备的

  • TMP的一些用处举例:

    • 在工程中,可以在编译期确保各种量纲正确

    • 优化矩阵运算

    • 可以生成用户定制之设计模式

      此处运用尚不清晰


Template metaprogramming可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率

TMP可被用来生成 “基于policy选择组合 ”的客户定制代码,也可用来避免生成对某些特殊类型并不合适的代码


8. 定制new和delete

条款49:了解new-handler的行为
1
2
3
4
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
  • set_new_handler的参数是个函数指针,指向operator new无法分配内存时希望被调用的函数(称为new-handler);返回值是当前正在使用的new-handler

  • operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存

  • 设计一个良好的new-handler函数有以下几种选择:

    • 让更多内存可被使用,例如程序一开始就分配一大块内存,而后调用new-handler时返还给程序使用
    • 安装另一个new-handler,即调用set_new_handler函数换一个可能能成功分配内存的new-handler
    • 卸除new-handler,即给set_new_handler函数传入null,这样会operator new会在内存分配失败时抛出异常
    • 抛出bad_alloc,此种异常不会被operator new捕捉,因此会被传递到内存索求处
    • 不返回,通常调用abortexit
  • 下面是一个”minxin”风格的base class,来使继承它的类有自己专属的new-handler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // 资源处理类,支持基础的RAII操作
    class NewHandlerHolder {
    public:
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) { }
    ~NewHandlerHolder() { std::set_new_handler(hanlder); }
    private:
    std::new_handler handler;

    NewHandlerHolder(const NewHandlerHolder&); // 阻止copying,见条款14
    NewHandlerHolder& operator=(const NewHandlerHolder&);
    };

    // mixin风格的base class,用以支持class专属的set_new_handler
    // 此处的类型参数T无需被使用,它的目的仅仅是为了让继承自NewHandlerSupport的每一个class,
    // 拥有实体互异的NewHandlerSupport复件(或者说static成员变量currentHandler)
    template<typename T>
    class NewHandlerSupport {
    public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
    private:
    static std::new_handler currentHandler;
    };

    template<typename T>
    std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHander;
    }

    template<typename T>
    void* operator new(std::size_t size) throw(std::bad_alloc) {
    // 函数结束调用h的析构,自动变回原来的new-handler
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
    }

    // 以下将每一个currentHandler初始化为null
    template<typename T>
    std::new_handler NewHandlerSupport<T>::currentHandler = 0;

    // 使用时,只需继承即可
    class Widget: public NewHandlerSupport<Widget> {
    ...
    }
    • 上面这种Widget继承自一个模板基类,且该基类以Widget为类型参数的手法,称为CRTP(curiously recurring template pattern)
  • 古老的operator new在无法分配足够内存时返回null,可以通过new (std::nothrow)来调用

    • 但这种方式仍没有异常安全保证,因为它只保证分配内存给对象时不抛掷异常,而对象的构造函数中仍可能进行new操作,而这个new是没有保证的

set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用

Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常


条款50:了解new和delete的合理替换时机
  • 替换new和delete的理由如下
    • 为了检测运用错误
    • 为了收集动态分配内存之使用统计信息
    • 为了增加分配和归还的速度
    • 为了降低缺省内存管理器带来的额外空间开销
    • 为了弥补缺省分配器中的非最佳齐位,例如编译器自带的operator news并不保证对动态分配的double采用8-byte齐位
    • 为了将相关对象成簇集中,例如某个数据结构往往被一起使用,就可以被成簇集中在某个内存页上
    • 为了获得非传统的行为

有许多理由需要写个自定的 new和 delete,包括改善效能、对 heap运用错误进行调试、收集 heap使用信息


条款51:编写new和delete时需固守常规
  • operator new成员函数会被派生类继承,而派生类的大小通常比基类大,所以一般需要处理申请的size不正确的情况(例如在不正确时调用标准的operator new来解决问题)
  • C++保证删除null指针永远安全

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用 new-handler。它也应该有能力处理 0 bytes申请。Class专属版本还应该处理 “比正确大小更大的(错误)申请 ”

operator delete应该在收到 null指针时不做任何事。Class专属版本还应该处理 “比正确大小更大的(错误)申请 ”


条款52:写了placement new也要写placement delete
1
2
3
void* operator new(std::size_t size) throw(std::bad_alloc);  // 正常的operator new签名式
void* operator delete(void* rawMemory) throw(); // global作用域中的正常签名式
void* operator delete(void* rawMemory, std::size_t size) throw(); // class作用域中典型的签名式
  • 如果operator new接受的参数除了一定会有的那个size_t之外还有其他,便是一个所谓的placement new

    • C++标准库中有一种placement new版本,它的用途之一是在vector的未使用空间上创建对象

      1
      2
      #include <new>
      void* operator new(std::size_t size, void* pMemory) throw();
  • 如果内存分配成功后构造函数发生异常,那么运行期系统会寻找“ 额外参数个数和类型都与operator new相同 ”的某个operator deleteplacement delete)并调用以恢复旧观;若找不到,就什么也不做(于是导致内存泄漏)

  • placement delete只有在 ” 伴随placement new调用而触发的构造函数 “ 异常时才会被调用

  • 由于名称遮掩的问题(见条款33),再类中只定义placement new会导致global的operator new名称被遮掩从而不可用

    • 一种做法是,建立一个base class,内含所有正常形式的new/delete,然后利用继承机制和using声明式

当你写一个 placement new,请确定也写出了对应的 placement delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏

当你声明 placement new和 placement delete,请确定不要无意识地遮掩了它们的正常版本


9. 杂项讨论

条款53:不要轻忽编译器的警告

严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取 “无任何警告 ”的荣誉

不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息可能会消失


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

注意,这章落笔于2005年初

  • TR1代表”Technical Report 1”,这实际上是一份文档

  • 一些重要的TR1组件如下

    • 智能指针
    • tr1::function,用以表示任何可调用物
    • tr1::bind
    • Hash tables,例如tr1::unordered_map之类
    • 正则表达式
    • Tuples
    • tr1::array,一个“STL化”数组,其大小固定
    • tr1::mem_fn
    • tr1::reference_wrapper
    • 随机数生成工具
    • 数学特殊函数,包括Lagrange多项式,Bessel函数等
    • C99兼容扩充
    • 一些template编程技术
      • Type traits
      • tr1::result_of,用于指涉函数调用的返回类型

C++标准程序库的主要机能由STL、iostreams、locales组成。并包含C99标准程序库

TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外10个组件的支持

TR1自身只是一份规范。为获得 TR1提供的好处、你需要一份实物。一个好的实物来源是Boost


条款55:让自己熟悉Boost

Boost C++ Libraries


Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的 C++程序库开发。Boost在 C++标准化过程中扮演深具影响力的角色

Boost提供许多 TR1组件实现品,以及许多其他程序库


受笔者知识水平和表达能力所限,有些问题上难免出现疏漏,欢迎在评论区指正