Effective C++, 3rd

1.让自己习惯 C++

Accustoming yourself to C++

01.视 C++ 为一个语言联邦

View C++ as a federation of languages

  • C
  • 面向对象的 C++:类、封装、继承、多态
  • 模板 C++:泛型编程
  • STL:模板库

02.常量,枚举和内联优于宏定义

Prefer consts, enums, inlines to #defines

  • 对于单纯常量,以 const 对象或枚举
  • 对于形似函数的宏,用内联函数替换
    • 常量、枚举和内联更具封装性,可以限定作用域
    • 枚举比常量约束更多,不能为该常量创建指针或引用

03.尽可能使用常量

Use const whenever possible

  • 指定语义约束,即“不该被改动”的对象
    • 可帮助编译器侦测错误用法
  • const 在 * 左边,表示被指物是常量
    • 也可将 const 放在类型之前
    • 例如const widget *pw等同于widget const *pw
    • 指针所指东西不可被改动
  • const 在 * 右边,表示指针自身是常量
    • 指针不得指向不同的东西,但所指东西的值可以改动
  • const 在 * 两侧,表示被指物和指针自身都是常量
  • const 成员函数
    • 可作用于 const 对象,不可更改对象内任何非静态成员变量
    • 成员变量前加mutable,也可在 const 成员函数内部修改该成员变量
    • 当 const 和 non-const 成员函数有着实质等价的实现时,另 non-const 版本调用 const 版本避免代码重复

04.确定对象被使用前已先被初始化

Make sure that objects are initialized before they’re used

  • 对于内置类型手动初始化
  • 对于类,在构造函数中初始化成员变量
    • 赋值不等于初始化
    • 使用成员初始化列表列替换赋值动作,前者效率更高,后者先设初值再赋值
    • 可使用无参数构造函数来初始化
    • 对于多个构造函数,可添加私有成员函数,接收初始化参数,在函数内部使用赋值操作给成员变量“初始化”
    • 初始化顺序
    • 先基类再衍生类
    • 类内部,按照声明的顺序初始化,与成员初始化列表列操作顺序无关
    • 最好按照声明顺序初始化
    • 不同编译单元内的 non-local static 对象的初始化顺序未定义
      • static 对象包括全局对象、定义于命名空间作用域内的对象、类内、函数内,以及在文件作用域内被声明为 static 的对象
      • 函数内的 static 对象称为 local-static 对象,其他的则是 non-local static 对象
      • 程序结束时 static 对象会被自动销毁,即在 main 函数结束时调用他们的析构函数
      • 编译单元是产出单一目标文件的源码
      • 将每个 non-local static 对象移到自己的专属函数内,改函数返回对该对象的引用,保证该函数被调用期间,首次遇到该对象的定义时被初始化,即以函数调用替换直接访问 non-local static 对象

2.构造/析构/赋值运算

Contructors, destructors, and assignments operators

05.了解 C++ 默默编写并调用哪些函数

Know what functions C++ silently writes and calls

  • 编译器自动为类创建默认构造函数、拷贝构造函数、拷贝赋值操作和析构函数

06.明确拒绝不想用的编译器自动生成的函数

Explicitly disallow the use of complier-generated functions you do not want

  • 如果不想用编译器自动生成的函数,可将相应的成员函数声明为 private 并且不予实现
  • 可以继承 Uncopyable 这样的基类,但是可能会多重继承

    class Uncopyable {
    protected: // allow constructor and destructor for derived object
    Uncopyable() {}
    ~Uncopyable() {}
    private:
    Uncopyable(const Uncopyable&); //avoid copying
    Uncopyable& operator=(const Uncopyable&);
    };
    

07.声明多态基类析构函数为虚函数

Declare destructors virtual in polymorphic base classes

  • 包含虚函数的类需要额外的信息来实现虚函数:vptr(virtual table pointer)指向一个由函数指针构成的数组,称为 vtbl(virtual table),每个有虚函数的类都有一个相应的 vtbl
  • 析构顺序:先父类再子类,构造函数的调用顺序相反
  • 带有多态性质的基类应声明一个虚析构函数
  • 如果一个类带有任何虚函数,就声明一个虚析构函数
  • 类的设计目的不是作为基类使用,或者不是为了多态性,不应该声明虚析构函数

08.别让异常逃离析构函数

Prevent exceptions from leaving destructors

  • 如果析构函数内可能抛出异常,应该在析构函数内捕获异常,然后不传播或结束程序
  • 如果需要客户自定义异常的反应,类应该提供接口执行该操作

09.绝不在构造和析构过程中调用虚函数

Never call virtual functions during construction or destruction

  • 在构造和析构中不要调用虚函数没因为这类调用不会下降到衍生类,即调用的仍然是基类的实现

10.使 operator= 返回一个 *this 的引用

Having assignment operators return a reference to *this

  • 赋值相关运算(包括 operator=/+=、-=、*=)操作符返回一个 *this 的引用

11.在 operator= 中处理“自我赋值”

Handle assignment to self in operator=

  • 确保对象自我赋值时,operator= 行为良好,包括比较源对象和目标对象的地址、精心周到的语句顺序(先复制源对象,再执行删除),以及icopy-and-swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,行为仍然正确

12.复制对象的所有部分

Copy all parts of an object

  • 拷贝构造函数和拷贝赋值操作符都是 copying 函数
  • copying 函数应该确保复制“对象内的所有成员变量”和“所有基类成分”
  • 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将相同的东西抽象成一个函数,二者都调用这个函数

3.资源管理

Resource management

13.以对象管理资源

Use objects to manage resources

  • 为防止内存泄漏,建议使用 RAII(Resource Acquisition Is Initialization,资源取得时机就是初始化时机) 对象,它们在构造函数中获得资源并在析构函数中释放资源
  • 常用的 RAII 类是 shared_ptr 和 auto_ptr。前者的拷贝行为比较直观,后者的复制动作会转移资源的所有权:shared_ptr 有引用计数,但是无法打破环装引用
  • 参考智能指针一文

14.在资源管理类中小心复制行为

Think carefully about copying behavior in resource-managing classes

  • 复制 RAII 对象必须一并复制它锁管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为
  • 一般情况下,RAII 类的 copying 行为是:阻止 copying、实行引用计数法

15.在资源管理类中提供对原始资源的访问

Provide access to raw resources in resource-managing classes

  • APIs 往往要求访问原始资源,所以每一个 RAII 类应该提供一个接口可以获得其管理的资源
  • 对原始资源的访问可以是显示转换或隐式转换:一般显示转换比较安全,隐式转换对客户比较方便

16.在对应的 new 和 delete 采用相同形式

Use the same form in corresponding uses of new and delete

  • 调用 new 时使用[],那么对应调用 delete 时也调用[]
  • 调用 new 时没有使用[],那么也不该在调用 delete 时使用[]

17. 以独立语句将 newed 对象保存到智能指针

Store newed onjects in smart pointers in standalone statements

  • 以独立语句将 newed 对象保存在智能指针内。否则,抛出异常的时候,可能会导致内存泄漏

4.设计与声明

Designs and declarations

18.让接口易被正常使用,不易被误用

Make interfaces easy to use correctly and hard to use incorrectly

  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
  • “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任
  • shared_ptr 支持自定义删除器,可以防止 DLL 问题,可被用来自动解除互斥锁

19.把类设计看作类型设计

Treat class design as type design

在设计一个类之前,考虑以下问题

  • 新类型的对象如何被创建和销毁
  • 对象的初始化和对象的赋值该有什么样的差别:区分构造函数和赋值操作符的行为
  • 新类型的对象如果以值传递,意味着什么:取决于拷贝构造函数
  • 什么是新类型的“合法值”:确定需要做的错误检查工作
  • 新类型需要配合某个继承图系吗:受继承类的约束,如果允许被继承,析构函数是否为虚函数
  • 新类型需要什么样的转换:显示类型转换和隐式类型转换
  • 什么样的操作符和函数对此新类型是合理的:确定需要声明的函数,哪些是成员函数,哪些不是成员函数
  • 谁该调用新类型的成员:确定成员的属性(public/protected/private),也确定类之间的关系(所属,友元)
  • 什么是新类型的未声明接口
  • 新类型有多一般化:是否需要定义一个模板类
  • 真的需要一个新类型吗:是否可以为已有类添加非成员函数或模板来实现

20.常量引用传递优于值传递

Prefer pass-by-reference-to-const to pass-by-value

  • 值传递效率低,而且可能造成对象切割(slicing):值传递一个衍生类对象时,如果函数声明的是基类,那么调用的是基类的拷贝构造函数
  • C++ 编译器底层使用指针实现,不同情形使用不同的方式
    • 内置类型(如 int)采用值传递
    • STL 的迭代器和函数对象使用值传递
    • 其他的采用常量引用传递

21.必须返回对象时,不要返回引用

Don’t try to return a reference when you must return an object

  • 绝不要返回指针或引用指向一个 local stack 对象
  • 绝不要返回引用指向一个 heap-allocated 对象
  • 绝不要返回指针或引用指向一个 local static 对象而有可能同时需要多个这样的对象

22.声明数据成员为私有的

Declare data memebers private

  • 语法一致性:public 接口内的所有东西都是函数
  • 可细微划分访问控制、允诺约束条件获得保证
  • protected 并不比 public 更具封装性

23.成员函数优于非成员、非友元函数

Prefer non-member non-friend functions to member function

  • 将所有功能函数放在多个头文件内但隶属同一命名空间,使用者可以轻松扩展这一组功能函数
    • 在命名空间添加非成员非友元函数,以便为使用者提供方便的接口
  • 优先考虑非成员、非友元函数替换成员函数,可以增加封装性、包裹弹性和机能扩充性

24.当类型转换需应用到所有参数,声明为非成员函数

Declare non-member functions when type conversions should apply to all parameters

  • 如果需要为某个函数的所有参数(包括被 this 指针所指的隐喻参数)进行类型转换,那么这个函数必须是非成员函数
    • 编译器可对每一个实参执行隐式类型转换

25.考虑支持不抛异常的 swap 函数

Consider support for a non-throwing swap

  • 如果 std::swap 缺省实现对自定义的类或类模板的效率不足,试着做
    • 提供一个 public swap 成员函数,在函数内高效地置换两个对象值
    • 在类或模板所在的命名空间提供一个非成员的 swap 函数,在函数内调用上述 swap 函数
    • 如果正在编写一个类或类模板,让该类特化 std::swap,另其调用上述的 swap 函数
  • 如果调用 swap,确定包含using std::swap,然后不加任何 namespace 修饰符,直接调用 swap,编译器就会查找适当的 swap 函数并调用
  • 警告:成员函数 swap 不可抛出异常

5.实现

Implementations

26.尽可能推迟变量定义

Postpone variable definitions as long as possible

  • 尽可能延后变量定义式的出现,最好是延后到可以用有意义的参数进行始化
  • 对于循环,如果构造和析构的代码大于赋值操作,则将定义放在循环外

27.最小化 cast 操作

Minimize casting

  • C 风格的转换操作,将 expression 转换为 T:(T)expressionT(expression)
  • C++ 另外提供 4 种转换操作
    • const_cast<T>( expression )用来移除对象的常量性,唯一可以实现这个目的的 C++ 风格的转换操作符
    • dynamic_cast<T>( expression )用于执行“安全向下转换”,用于确定某对象是否归属继承体系中的某个类型,可能耗费重大运行成本,唯一一个 C 风格无法实现的转换操作
    • reinterpret_cast<T>( expression )意图执行低级转换,实际动作和结果可能取决于编译器,即不可移植
    • static_cast<T>( expression )用于强迫隐式转换,例如 non-const 转换为 const,或者 int 转 double 等
  • 倾向使用 C++ 风格的转换操作,不要使用 C 风格的转换
    • 易被辨识,因而得以简化查找类型被破坏的过程
    • 各转换工作有各自的局限,便于编译器诊断错误的运用
  • 如果可以,尽量避免转换操作,特别是在注重效率的代码中避免 dynamic_cast,如果有需要,尝试改成无需转换的设计
    • 使用类型安全容器,确定是哪种衍生类或基类
    • 将虚函数放在父类,然后添加空实现
  • 如果必须转换,试着用函数封装,可以调用函数,而无需将转换操作引入代码

28.避免返回指向对象内部的句柄

Avoid returning “handles” to object internals

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

29.努力写异常安全的代码

Strive for exception-safe code

  • 异常安全函数即使发生议程也不会内存泄漏或破坏任何数据结构。这样的函数分为三种可能的保证:基本型、强烈型、不抛异常型
  • “强烈保证”往往以 copy-and-swap 实现,但“强烈保证”并非对所有函数都可实现或具备现实意义
  • 函数提供的“异常安全保证”通常最高只等于其调用的各个函数的“异常安全保证”中的最弱者

30.了解内联的细节

Understand the ins and outs of inlining

  • 将大多数内联限制在小型、被频繁调用的函数。可使日后的调试过程和二进制升级更容易,也可最小化潜在的代码膨胀问题,最大化提升程序的速度
    • 内联函数无法随着程序库的升级而升级:内联函数修改,用到该函数的程序必须重新编译
    • 大部分调试器不支持内联函数调试
  • 隐式内联:函数定义在类定义内
  • 显式内联:添加关键字 inline
    • 没有要求每个函数都是内联,就避免声明一个模板是内联
  • 大多数编译拒绝复杂的函数内联:比如虚函数,带有循环或递归的函数。此时会有警告信息
  • 编译器通常不对“通过函数指针进行的调用”执行内联
  • 不要只因为函数模板出现在头文件,就将其声明为内联

31.最小化文件编译依赖

Minimize compilation dependencies between files

  • pimply idiom(pointer to implementation):将一个类分为两个,一个提供接口,一个负责实现接口,前者在类内包含一个后者的 shared_ptr,做到“接口与实现分离”
  • 使用接口类、衍生类和工厂模式进行实现
  • 分离的关键在于“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,万一做不到,则使用前置声明
  • 设计策略
    • 尽量使用对象引用或对象指针,而不是对象:可以在头文件中使用前置声明
    • 尽量使用 class 声明式而不是 class 定义式
    • 为声明式和定义式提供不同的头文件
  • 程序头文件应该以“完全且仅有声明式”的形式存在

6.继承与面向对象设计

Inheritance and object-oriented design

32.确保公有继承是”is-a”关系

Make sure public inheritance models “is-a”

  • public 继承意味着 is-a。适用于基类的每一件事情一定适用于衍生类,每一个衍生类对象也都是一个基类对象

33.避免隐藏继承的名字

Avoid hiding inherited names

  • 衍生类内的名称会隐藏基类内的名称
    • 如果继承基类并加上重载函数,又希望重新定义或覆盖其中一部分,必须为那些原本会被隐藏的名称引入一个 using 声明式,否则继承的名称会被隐藏
  • 为了让隐藏的名称仍然可见,可使用 using 声明式或 forwarding 函数
    • 内置的 forwarding 函数的另一个用途是为那些不支持 using 声明式的编译器而用

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

Differentiate between inheritance of interface and inhertance of implementation

  • 接口继承和实现继承不同。在 public 继承时,衍生类会继承基类的接口,即成员函数
  • 声明纯虚函数的目的是让衍生类只继承函数接口
  • 声明非纯虚函数的目的是让衍生类继承该函数的接口和缺省实现
  • 声明非虚函数的目的是让衍生类继承函数的接口和一份强制性实现

35.考虑虚函数的替代

Condider alternatives to virtual functions

  • 虚函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式
    • 使用 non-virtual interface(NVI)手法,是 Template Method 设计模式的一种特殊形式。以 public non-virtual 成员函数包裹较低访问性的虚函数
    • 将虚函数替换为“函数指针成员变量”。是 Strategy 设计模式的一种分解表现形式
    • 以 function 成员变量替换虚函数,因而允许使用任何可调用实体(callable entities)搭配一个兼容与需求的签名式。这也是 Strategy 设计模式的某种形式
    • 将继承体系内的虚函数替换为另一继承体系的虚函数。这是 Strategy 设计模式的传统实现手法
  • 将功能从成员函数移到类外部,缺点是非成员函数无法访问类的 non-public 成员
  • function 对象的行为就像一般函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用实体

36.绝不重定义继承的非虚函数

Never redefine an inherited non-virtual function

  • 非虚函数是静态绑定的,虚函数是动态绑定的
  • 任何情况下都不该重新定义一个继承而来的非虚函数,否则调用的函数取决于对象最开始的声明类型,跟实际所指类型无关

37.绝不重定义函数继承的默认参数值

Never redefine a function’s inherited default parameter value

  • 虚函数是动态绑定,但是缺省参数是静态绑定
    • 调用虚函数时,默认参数可能是基类的默认参数,而不是实际指向的父类的默认参数
  • 静态类型是声明的类型,动态类型是“目前所指对象的类型”
    • 动态类型可以表现出一个对象将会有什么行为
    • 动态类型可在程序执行过程中改变
  • 可以使用 NVI 手法:另基类内的一个 public 非虚函数调用 private 虚函数,后者可被衍生类重新定义。让非虚函数知道缺省参数,虚函数负责真正的工作

38.通过组合对”has-a”或”is-implemented-in-terms-of”建模

Model “has-a” or “is-implemented-in-terms-of” through composition

  • 复合是类型间的一种关系,当某种类型的对象内包含其他类型的对象,就是复合关系
  • 在应用域,复合意味着 has-a(有一个)。在实现域,复合以为着 is-implemented-in-terms-of(根据某物实现出)

39.慎重使用私有继承

Use private inheritance judiciously

  • private 继承意味着 is-implemented-in-terms-of。通常比复合的级别低,但是当衍生类需要访问基类的 protected 成员,或需要重新定义继承而来的虚函数时,private 继承是合理的
    • private 继承时,编译器不会自动将一个衍生类对象转换为一个基类对象
    • 由 private 继承而来的所有成员,在衍生类中都是 private 属性
    • private 继承是一种实现技术,意味着只有实现部分被继承,接口部分应忽略
  • 与复合相比,private 继承可以使得空白基类最优化(EBO, empty base optimization)。对致力于“对象尺寸最小化”的程序库开发者比较重要
  • 尽可能使用复合,必要时采用 private 继承
    • 当想要访问一个类的 protected 成员,或需要重新定义该类的一个或多个虚函数
    • 当空间更加重要,衍生类的基类可以不包含任何 non-static 成员变量
    • “独立(非附属)”对象的大小一定不为零,不适用于单一继承(多重继承不可以)衍生类对象的基类

40.慎重使用多重继承

Use multiple inheritance judiciously

  • 多重继承是继承一个以上的基类,但这些基类并不常在继承体系中又有基类
    • 虚继承:防止多重继承时,基类之间又有基类,从而上层的基类的成员变量被父类复制
    • 虚继承的类产生的对象体积更大,访问虚基类的成员变量速度慢,增加初始化(及赋值)的复杂度
    • 如果虚基类不带任何数据,是具有使用价值的情况
  • 多重继承比单一继承复杂,可能导致新的歧义性,以及对虚继承的需要
  • 多重继承的用途:涉及“public 继承某个接口类”和“private 继承某个协助实现的类”

7.模板与泛型编程

Templates and generic programming

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

Understand implicit interfaces and compile-time polymorphism

  • 类和模板都支持接口和多态
  • 对类而言接口是显式的,以函数签名为中心。多态则是通过虚函数发生于运行期
  • 对模板参数而言,接口是隐式的,基于有效表达式。多态则是通过模板具体化和函数重载解析,发生于编译期

42.理解 typename 的双重定义

Understand the two meanings of typename

  • 声明模板类型参数的两种方式:
    • template<class T> class widget;
    • template<typename T> class widget;
  • 从属名称:模板内的名称依赖于某个模板参数
    • 非从属名称:模板内不依赖模板参数的名称
  • 嵌套从属名称:从属名称在类内呈嵌套状
  • 嵌套从属类型名称:嵌套从属名称且指向某类型
    • 想在模板中指定一个嵌套从属类型名称,就必须在紧邻它的前一个位置加上关键字 typename
    • typename 不可出现在基类列表类的嵌套从属类型名称前,也不可在成员初始化列表中作为基类的修饰符

43.了解如何访问模板化基类内的名称

Know how to access names in templatized base classes

  • 当基类从模板中被具体化时,它假设对基类的内容一无所知,即衍生类基类继承一个基类模板,不能再衍生类的实现中直接调用基类的成员(变量和函数)
    • 可在衍生类模板内添加this->指向基类模板的成员(变量和函数)
    • 使用 using 声明式,假设已经存在这个成员(变量和函数)
    • 明确指出被调用的函数位于基类内,使用基类::,如果是一个虚函数,会关闭虚函数的动态绑定行为

44.把参数无关的代码分离出模板

Factor parameter-independent code out of templates

  • 模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的模板参数产生依赖关系
  • 因非类型模板参数造成的代码膨胀,往往可以消除,做法是以函数参数或类成员变量替换模板参数
  • 因类型参数造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表示的具体类型实现共享代码

45.使用成员函数模板来接受“所有兼容类型”

Use member function templates to accept “all compatible types”

  • 具有基类-衍生类关系的两个类型分别具体化某个模板,生成的两个结构并不带有基类-衍生类关系
  • 使用成员函数模板生成“可接受所有兼容类型”的函数
  • 如果声明成员模板用于“泛化拷贝构造”或“泛化赋值操作”,必须声明正常的拷贝构造函数和拷贝赋值操作符
    • 声明泛化拷贝构造函数和拷贝赋值操作符,不会阻止编译器生成默认的拷贝构造函数和拷贝赋值操作符

46.需要类型转化时在模板内定义非成员函数

Define non-member functions inside templates when type conversions are desired

  • 模板实参推导过程中不会考虑隐式类型转换函数
  • 写类模板时,当它提供的“与此模板相关的”函数支持“所有参数的隐式类型转换”时,将那些函数定义为类模板内部的友元函数
    • 在类内部声明非成员函数作为友元函数,成为内联函数
    • 为了将内联声明的影响最小化,在类外定义一个辅助函数模板,在友元函数内只调用辅助函数

47.使用 traits class 表现类型信息

Use traits classes for information about types

  • STL 有 5 种迭代器
    • input 迭代器:只能向前移动,一次异步,只可读取(不能修改)所指的东西,且只能读取一次。模仿了指向输入文件的读指针。如 C++ 的 istream_iterator
    • output 迭代器:只能向前移动,一次一步,只可修改所指的东西,且只能修改一次。模仿了指向输出文件的写指针。如 C++ 的 ostream_iterator
    • input 和 output 迭代器都只适合“单步操作算法(one-pass algorithms)”
    • forward 迭代器:既能完成上述两种迭代器的工作,且可以读或写所指对象一次以上。使得可以实施“多步操作算法(multi-pass algorithms)”。如单向链表的迭代器
    • bidirectional 迭代器:既能完成 forward 迭代器的工作,还支持向后移动。STL 的 list/set/multiset/map/multimap 迭代器就属于这一分类
    • random access 迭代器:可以执行“迭代器运算”,即可以在常量时间内向前或向后跳跃任意距离。如 array/vector/deque/string 提供的都是随机访问迭代器
  • 如何设计一个 traits 类
    • 确认若干希望将来可取得的类型相关信息。例如迭代器希望取得分类(category)
    • 为该信息选择一个名词。如迭代器是 iterator_category
    • 提供一个模板和一组特化版本,其中包含希望支持的类型相关信息
    • traits 类的名称常以”traits”结束
  • 如何使用一个 traits 类
    • 建立一组重载函数(类似劳工)或函数模板,彼此间的差异只在于各自的 traits 参数。令每个函数实现与其接受的 traits 信息相对应
    • 建立一个控制函数(类似工头)或函数模板,调用上述的函数并传递 traits 类所提供的信息
  • traits 类使得“类型相关信息”在编译期可用。它们以模板和一组“模板特化”完成实现
  • 整合重载技术后,traits 类可在编译期对类型执行 if…else 测试

48. 认识模板元编程

Be aware of template metaprogramming

  • 模板元编程(TMP, template metaprogramming)是编写基于模板的 C++ 程序并在编译期执行的过程
    • 即以 C++ 写成、在 C++ 编译期内执行的程序
    • TMP 程序结束执行,输出的 C++ 源码可以像往常一样编译
    • 优点:
    • 让某些事情更容易
    • 可将工作从运行期转移到编译期。使得原本在运行期才可以侦测的错误在编译期被找到
    • TMP 的 C++ 程序在每一方面可能更加高效:较小的可执行文件、较短的运行期、较少的内存需求
    • 缺点:导致编译时间变长
  • TMP 主要是函数式语言,可以达到的目的
    • 确保度量单位正确:在编译期确保程序所有度量单位的组合是正确的
    • 优化矩阵运算:使用 expression template,可能会消除中间计算生成的临时对象并合并循环
    • 可生成用户自定义设计模式的实现品。设计模式如 Strategy/Observer/Visitor 等都可以多种方式实现
  • 问题:
    • 语法不直观
    • 支持工具不充分,如没有调试器

8.定制 new 和 delete

Customizing new and delete

  • newdelete只适合分配单一对象;new []delete []用来分配数组
  • STL 容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,而不是 new 和 delete 管理

49.理解 new-handler 的行为

Understand the behavior of the new-handler

  • 当 new 操作抛出异常以反映一个未获满足的内存需求之前,会先调研一个客户指定的错误处理函数,即 new-handler
    • 可以用是set_new_handler设置该函数
    • 参数是个指针,指向 new 无法分配足够内存时该调用的函数
    • 返回值是个指针,指向set_new_handler被调用之前正在执行的 new_handler 函数
    • new_handler 是个 typedef,定义一个指针指向函数,函数没有参数也没有返回值
  • 设计良好的 new-handler 函数
    • 让更多内存可被使用:程序一开始执行就分配一大块内存,而后第一次调用 new-handler,将该内存释放给程序使用
    • 设置另一个 new-handler:如果已知哪个 new-handler 可以获得更多可用内存,调用时设置该 new-handler 替换自己。比如令 new-handler 修改“会影响 new-handler 行为”的静态数据、命名空间数据或全局数据
    • 取消设置 new-handler:即将 null 指针传给set_new_handler,内存分配不成功时就会抛异常
    • 抛出 bad_alloc 或派生自 bad_alloc 的异常:该异常不会被 new 操作捕获,但会传播给请求内存的代码
    • 不返回:通常调用 abort 或 exit
  • nothrow new是一个有局限性的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常

50.理解何时替换 new 和 delete 有意义

Understand when it makes sense to replace new and delete

  • 三个替换编译器提供的 new 和 delete 理由:
    • 检测运用上的错误:自定义 new 操作,可超额分配内存,以额外空间放置特定的 byte patterns(即签名,signature)。对应的 delete 操作可以检查上述签名是否原封不动,若否表示在分配区的某个声生命时间点发生了 overrun(写入点在分配区块尾端之后) 或 underrun(写入点在分配区块起点之前)。此时 delete 可以日志记录该时间和发生错误的指针
    • 强化效能:编译器的 new 和 delete 无法解决碎片问题,导致程序可能无法申请大区块内存。通常来说这种自定制的性能更好
    • 收集使用上的统计数据:先收集软件如何使用动态内存,包括分配区块的大小分布、寿命分布、分配和释放的次序(FIFO/LIFO/随机)、任何时刻内存分配上限
    • 增加分配和释放的速度:当定制型分配器专门针对某特定类型的对象设计时,往往比泛用型分配器更快
    • 降低缺省内存管理器带来的空间额外开销:泛用型内存管理器往往使用更多内存
    • 弥补缺省分配器中的非最佳对齐:缺省的分配器一般是 4 字节对齐,但是对于 x86 最好是 8 字节对齐
    • 将相关对象成簇集中:将往往被一起使用某个数据结构放在一起创建,可以减少 page fault 的错误
    • 获得非传统的行为:比如添加数据初始化工作

51.写 new 和 delete 时遵循惯例

Adhere to convention when writing new and delete

  • new 操作
    • 应该包含一个无穷循环,并在其中尝试分配内存
    • 如果无法满足需求,调用 new-handler
    • 也应该可以处理 0 字节申请
    • 类的自定义版本还应该处理“比正确大小更大的(错误)申请”
  • delete 操作
    • 收到 null 指针不做任何事
    • 类的自定义版本还应该处理“比正确大小更大的(错误)申请”

52.写了 placement new 也要写 placement delete

Write placement delete if you write placement new

  • 如果自己实现一个 placement operator new,也要写出对应的 placement operator delete。否则会发生隐蔽时断时续的内存泄漏
  • 当声明 placement new 和 placement delete,确定不要无意识地遮掩它们的正常版本

9.杂项讨论

Miscellany

53.注意编译器警告

Pay attention to compiler warnings

  • 严肃对待编译器发出的警告信息。努力在编译器的最高(最严苛)警告级别下争取“无任何警告”
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度不相同。一旦移植到另一个编译器上,原本依赖的警告信息有可能消失

54.熟悉包括 TR1 在内的标准库

Familiarize yourself with the standard library, including TR1

55.熟悉 Boost

Familiarize yourself with Boost

相关