0%

Effective C++之实现

5章:实现

条款26:尽可能延后变量定义式的出现时间
  • 应该延长变量的定义,直到非得使用它时
  • 应该延后这份定义,直到能够给它初值实参

对于循环存在的情况:

  • 做法A:定义于循环外

    1
    2
    3
    4
    5
    6
    Widget w;
    for(int i = 0; i < n; i++)
    {
    w = 取决于 i 的某个值;
    ...
    }
  • 做法B:定义于循环内

    1
    2
    3
    4
    5
    for(int i = 0; i < n; i++)
    {
    Widget w(取决于 i 的某个值);
    ...
    }

    这两种写法的成本如下:

    • 做法A1个构造函数 + 1个析构函数 + n个赋值操作

    • 做法Bn个构造函数 + n个析构函数

    • 做法A会造成名称w的作用域比做法B更大

      除非(1)你知道赋值成本比构造 + 析构成本低,(2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B

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

旧式C转型:

  • T(expression)
  • (T)expression

新式C++转型:

  • const_cast<T>(expression)

    用来将对象的常量性(const)转除(只有它能办到)。

  • dynamic_cast<T>(expression)

    用来执行安全向下转型,也就是用来决定某对象是否归属继承体系中的某个类型。

  • reinterpret_cast<T>(expression)

    用来执行低级转型,如将一个pointer to int 转为一个int。很少使用(在条款50中使用过一次)。

  • static_cast<T>(expression)

    用来强迫隐式转换,例如将non-const对象转为const对象,将int转为double,将void*指针转为typed指针,或将pointer to base转为pointer to derived

任何一种类型转换(无论显式转换还是隐式转换)往往都会令编译器编译出运行期间执行的码。如将int转为double会产生一些代码,因为int的底层表述不同于double的:

1
2
3
int x, y;
...
double z = static_cast<double>(x) / y;

再比如:

1
2
3
4
class Base {...};
class Derived: public Base {...};
Derived d;
Base* pb = &d; // 隐喻的将 Derived* 转换为 Base*

这种情况下有时候会有一个偏移量在运行期被施行于Derived*身上,用来取得正确的Base*指针值。这个例子表明单一对象可能有一个以上的地址(以Base*指向它时的地址和以Derived*指向它时的地址)。

假设我们有个base class Window和一个derived class Specialwindow,两者都定义了virtual函数onResize。进一步假设SpecialwindowonResize函数被要求首先调用WindowonResize。下面实际上是错的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Window
{
public:
...
virtual void onResize () {...} // base onResize实现代码
...
};

class Specialwindow: public Window
{
public:
...
// derived onResize 实现代码
virtual void onResize()
{
static_cast<Window>(*this).onResize(); // 将 *this 转型为 Window,
// 然后调用其 onResize;
// 这不可行!
... // 这里进行 Specialwindow 专属行为
}
...
};

它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象之base class成分”的暂时副本身上的onResize!如果Window::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。

正确的做法是:

1
2
3
4
5
6
7
8
9
10
11
class Specialwindow: public Window
{
public:
...
virtual void onResize()
{
Window::onResize(); // 调用 Window::onResize 作用于 *this 身上
...
}
...
};
  • dynamic_cast的实现版本执行速度相当慢,应该在注重效率的代码中保持对dynamic_cast的警觉
  • 优良的C++代码很少使用转型,我们应该尽可能隔离转型动作,通过将它隐藏在某个函数内,使得客户可以调用该函数而不需要将转型放进他们自己代码内
  • 宁可使用新式转型,也不要使用旧式转型,前者很容易辨识出来
条款28:避免返回handles指向对象内部成分
  • handles指指针、引用和迭代器
  • 成员变量的封装性最多只等于“返回其reference”的函数的访问级别
  • 如果const成员函数不得不传出去一个reference,则最好将返回类型限定为const
  • 有可能会导致空悬的handles,它的生命期却长于其所指对象
条款29:为“异常安全”而努力是值得的

当异常被抛出时,带有异常安全性的函数会:

  • 不泄露任何资源
  • 不允许数据败坏

较少的码就是较好的码,因为出错机会比较少,而且一旦有所改变,被误解的机会也少。异常安全码必须提供以下三个保证之一:

  • 基本承诺

    如果异常被抛出,程序内的任何事物仍然保证在有效状态(只要那是个合法状态)下,没有任何对象或数据结构会因此而败坏,所有对象处于一种内部前后一致的状态。

  • 强烈保证

    如果异常被抛出,程序状态不改变。

  • 不抛掷保证

    承诺绝不抛出异常,因为它们总能完成原先承诺的功能。

可以实现强烈保证的copy and swap技术:为你打算修改的对象做出copy一份副本,然后在副本身上做一切修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。带所有改变都成功后,再将修改后的副本和原对象swap

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
// 之所以实现为 struct 是因为其 PrettyMenu 的数据封装性
// 已经由 pImpl 是 private 获得了保证
class Image {...};

struct PMImpl
{
std::shared_ptr<Image> bgImage;
int imageChanges;
};

class PrettyMenu
{
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl; // pimpl idiom 详细描述见条款 31
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
// 见条款25
Lock ml(&mutex);
// 获得mutex的副本数据
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); // 修改副本
++pNew->imageChanges;
swap(pImpl, pNew); // 置换(swap)数据, 释放 mutex
)

注意,并非所有函数都可实现强烈保证或其具备现实意义。

函数提供的”异常安全保证“通常最高只等于其所调用各个函数提供的”异常安全保证“中的最弱者。

条款30:透彻了解inlining的里里外外

inline函数是指将对此函数的每一个调用都以函数本体替换之。这将导致两种情况:

  • 目标码增加(显然)
  • 如果inline函数的体积很小,编译器对“函数本体”所产出的码可能比“函数调用”所产出的码小

inline只是对编译器的一个申请,不是强制命令,也就是说编译器可以拒绝将太过复杂(带有递归或循环)的函数进行inline。通过对函数使用inline关键字属于明确提出申请,将函数定义于类内属于隐喻申请。

  • 将大多数inline限制在小型、被频繁调用的函数身上

  • inline函数通常一定被置于头文件内

    因为大多数构建环境在编译过程中进行inlining,而为了将一个函数调用替换为被调用函数的本体,编译器必须知道那个函数长什么样子。

  • 所有对virtual函数的inline申请都会被拒绝(因为对virtual函数的调用在运行期才确定)

条款31:将文件间的编译依存关系降至最低
  • 将接口从实现中分离

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person
    {
    public:
    Person(const string& name, const Date& birthday, const Address& addr);
    string name() const;
    string birthDate() const;
    string address() const;
    ...
    private:
    string theName; // 实现细目
    Date theBirthDate; // 实现细目
    Address theAddress; // 实现细目
    };

    这里的class无法通过编译,因为编译器没有取得其实现代码所用到的class stringDateAddress的定义式,通常应该在Person定义文件的最上方存在include头文件。

    1
    2
    3
    #include <string>
    #include "date.h"
    #include "address.h"

    不幸的是,这么一来便是在Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用person class的文件也必须重新编译。这样的连串编译依存关系(cascading compilation dependencies)会对许多项目造成难以形容的灾难。

  • Handle classespimpl idiom(pointer to implenmentation) + 前向声明

    Person分割成两个class,一个只提供接口,另一个负责实现接口。

    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
    #include <string> //标准程序库组件不该被前向声明
    #include <memory>

    // 使用前向声明而不是包含头文件, 这个很关键
    class PersonImpl; // Person 实现类

    // Person 接口用到的 class
    class Date;
    class Address;

    class Person
    {
    public:
    Person(const string& name, const Date& birthday, const Address& addr);
    string name() const;
    string birthDate() const;
    string address() const;
    ...
    private:
    shared_ptr<PersonImpl> pImpl; // 指针, 指向实现物
    // 如果不这样做的话, 编译器在编译 Person 类的时候,必须知道 Person 类的大小
    // 就不得不包含这三个实现细目
    /*
    string theName; // 实现细目
    Date theBirthDate; // 实现细目
    Address theAddress; // 实现细目
    */
    // 然而一旦这样写, 前面的前向声明就没用了, 必须包含头文件
    };

    这样一来,Person类的使用者就完全于DateAddress以及Person的实现细目相分离了。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:实现上让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。

    几个简单的设计策略:

    • 如果使用object referenceobject pointer可以完成任务,就不要使用object

      你可以只靠一个类型声明式就定义出指向该类型的referencepointer;但如果定义某类型的object,就需要用到该类型的定义式。

    • 如果能够,尽量以class声明式替换class定义式

      注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义。

    • 为声明式和定义式提供不同的头文件

      对于前面代码中的前向声明,最好是提供一个声明式头文件,包含全部的前向声明。

      下面式Person.cpp文件的部分实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <Person.h>
      #include <PersonImpl.h>
      // 注意,Person 和PersonImpl的成员函数完全相同, 两者接口完全相同

      Person::Person(const string& name,
      const Date& birthday,
      const Address& addr) : pImpl(new PersonImpl(name, birthday,addr)) {}

      string Person::narne( ) const {return pImpl->name();}
      ...
  • Interface classesabstract base class + factory函数

    abstract base class通常没有成员变量,也没有构造函数,只有一个virtual析构函数和一组pure virtual函数。但Interface class的客户必须有办法为这种class创建新对象。他们通常调用一个特殊函数,此函数创建实际实现的derived class,这样的函数通常称为factory(工厂)函数(见条款13),它们返回智能指针指向动态分配所得对象,而该对象支持Interface class的接口。这样的函数又往往在Interface class内被声明为static

    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
    // 位于 Person.h
    // 抽象基类, 只有声明
    // 提供给客户使用的头文件
    class Person
    {
    public:
    virtual ~Person();
    virtual strng name() const = 0;
    virtual strng birthDate() const = 0;
    virtual string address() const = 0;
    // static 的 factory 函数
    static shared_ptr<Person> create(const string& name,
    const Date& birthday,
    const Address& addr);
    ...
    }

    // 位于 Person.cpp
    #include "Person.h"

    class RealPerson: public Person
    {
    public:
    RealPerson(const string& name,
    const Date& birthday,
    const Address& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {}
    virtual ~RealPerson() {}
    string name() const;
    string birthDate() const;
    string address() const;
    private:
    string theName;
    Date theBirthDate;
    Address theAddress;
    };

    ... // 虚函数的实现码

    shared_ptr<Person> Person::create(const string& name,
    const Date& birthday,
    const Address& addr)
    {
    return shared_ptr<Person>(new RealPerson(name, birthday, addr));
    }

Handle classesInterface classes解除了接口和实现之间的耦合关系,从而降低了文件间的编译依存性。