0%

条款32:在未来时态下发展程序

  1. 请避免”demand-apged“式的虚函数,那会使你习惯于”不让任何函数成为virtual,除非有人需要“。应该有自己的判断,决定函数的意义,而不要只为了图某人的方便就改变其定义。请确定所做的改变对于整个class的上下关系乃至它所表现的抽象性是合理的。
  2. 请为每个class处理assignment和copy construction动作,即使没有人使用那个动作。如果不易完成,请声明为private,那就不会有人因为不经意调用编译器自动产生却行为错误的版本。
  3. 不要做出令人大吃一惊的怪异行为:努力让classes的操作符和函数拥有自然的语法和直观的语义。请和内置类型的行为保持一致。请让你的classes容易被正确地使用,不容易被误用。
  4. 请努力写出可移植代码
  5. 设计你的代码,使系统改变所带来的冲击得以局部化。尽量采用封装性质,尽可能让实现细目成为private。如果可用,尽量用匿名namespaces或文件内的static对象和static函数。尽量避免设计出virtual base classes,因为这种casses必须被其每个derived classes初始化。请避免以RTTI作为设计基础并因而导致一层层的if-then-else语句,因为灭当class继承体系有改变,每一组这样的语句都得更新,而忘了其中某一项,编译器并不会给出警告。

条款33:将非尾端类(non-leaf classes)设计为抽象类 (abstract classes)

  1. 有个类继承体系:
    1
    2
    3
    graph BT
    Lizard --> A(Animal)
    Chicken --> A
    Animal负责具体化所有东吴的共同特征,Lizard和Chicken是需要特殊对待的两种动物,它们的简化定义像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Animal {
    public:
    Animal& operator=(const Animal& rhs);
    ...
    };
    class Lizard: public Animal {
    public:
    Lizard& operator=(const Lizard& rhs);
    ...
    };
    class Chicken: public Animal {
    public:
    Chicken& operator=(const Chicken& rhs);
    ...
    };
    考虑一段代码:
    1
    2
    3
    4
    5
    6
    Lizard liz1;
    Lizard liz2;
    Animal *pAnimal1 = &liz1;
    Animal *pAnimal2 = &liz2;
    ...
    *pAnimal1 = *pAnimal2;
    其中有两个问题。
    • 最后一行调用Animal class的assignment操作符,即使对象类型是Lizard。而只有例子
      的Animal成分被修改,这就是所谓的部分复制partial assignments)。在这个动作之后,liz1的Animal members于liz2相同,但是lizard members则没变化。
    • 很多人用指针这样写。但复制动作很容易错用。
    解决方法是让assignment操作符为virtual
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Animal {
    public:
    virtual Animal& operator=(const Animal& rhs);
    ...
    };
    class Lizard: public Animal {
    public:
    virtual Lizard& operator=(const Animal& rhs);
    ...
    };
    class Chicken: public Animal {
    public:
    virtual Chicken& operator=(const Animal& rhs);
    ...
    };
  2. assignment操作符为了返回一个正确的reference类型,代表class,使得在class中为虚函数声明完全相同的参数类型(Animal)。意味着任何种类的Animal对象都可以出现在assignment操作符的右边:
    1
    2
    3
    4
    5
    6
    lizard liz;
    Chicken chick;
    Animal *pAnimal1 = &liz;
    Animal *pAnimal2 = &chick;
    ...
    *pAnimal1 = *pAnimal2; // 将鸡赋值给蜥蜴
    这就是异型赋值。语言的强烈类型检验(strong typing)通常会将它们视为不合法。
    这使我们为难。一方面希望通过指针同型赋值,但又禁止通过那样的指针进行异型赋值。要区分这些情况,得在运行期才有办法,dynamic_cast可以协助完成上述愿望
    1
    2
    3
    4
    5
    6
    7
    Lizard& Lizard::operator=(const Animal& rhs)
    {
    // 确定rhs真的是一只蜥蜴
    //将rhs转为const Lizard&,失败则抛出bad_cast exception
    const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
    // proceed with a normal assignment of rhs_liz to *this;
    }
    同型类型不需要dynamic_cast,成本太高,通过别的实现方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Lizard : public Animal {
    public:
    virtual Lizard& operator=(const Aniaml &rhs);
    Lizard& operator=(const Lizard &rhs); // 加上这行
    ...
    };

    Lizard& Lizard::oeprator=(const Animal &rhs)
    {
    // 转型成功,调用正常的assignmen操作符,否则抛出bad_cast exception
    return operator=(dynamic_cast<const Lizard&>(rhs));
    }
    运行时期的所有类型检验动作,以及对dynamic_casts的各种运用,都有其固有的缺点:某些编译器不支持dynamic_cast不说,它竟然要求每一次动作都需要待命捕捉bad_cast exceptions。为此,将clients可能在一开始就出现问题的赋值动作扼杀在编译器。阻止这种赋值动作的最简单方法就是,让operator=成为Animal的private函数:
    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
    class Animal {
    private:
    Animal& operator=(const ANimal &rhs);
    ...
    };

    class Lizard : public Animal {
    public:
    Lizard& operator=(const Lizard &rhs);
    ...
    };

    class Chicken : public Animal {
    public:
    Chicken& operaotr=(const Chicken &rhs);
    ...
    };

    Lizard liz1, liz2;
    ...
    liz1 = liz2; // good
    Chicken chick1, chick2;
    ...
    chick1 = chick2;// good

    Animal *pAnimal1 = &liz1;
    Animal *pAnimal2 = &chick1;
    ...
    *pAnimal = *pAnimal2; // error!企图调用private Animal::operaor=
    为了完成Animal对象间的相互赋值,可把private声明为protected,然而,前一个问题又出现了。因此,最简单的办法就是消除Animal对象之间相互赋值的需要,以下是新的继承体系:
    1
    2
    3
    4
    graph BT
    Lzard --> A(AbstractAnimal)
    Animal --> A
    Chicken --> A
    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
    class AbstractAnimal {
    protected:
    AbstractAnimal& operator=(const AbstractAnimal &rhs);
    public:
    virtual ~AbstractAniaml() = 0;
    ...
    };

    class Aniaml : public AbstractAnimal {
    public:
    Animal& operaotr=(const Animal &rhs);
    ...
    };

    class Lizard : public AbstractAnimal {
    public:
    Lizard& operator=(const Lizard &rhs);
    ...
    };

    class Chicken : public AbstractAnimal {
    public:
    Chicken& operaotr=(const Chicken &rhs);
    ...
    };
    剩余分析过程实际上体现了这样一种思想:当具体类被当做基类使用时,应该将具体类转变为抽象基类
    然而有时候需要使用第三方库,并继承其中一个具体类,由于无法修改该库,也就无法将该具体类转为抽象基类,这是就需要采取其他选择:
    • 将具体类继承自既存的(程序库)具体类,注意assignment相关问题,和条款3的数组陷阱
    • 试着在继承体系中找一个更高层的抽象类,然后继承它
    • 以”所希望继承的那么程序库类”来实现新类.例如使用复合或private继承并提供相应接口.此策略不具灵活性
    • 为”所希望继承的那么程序库类”定义一些non-member,不再定义新类

条款34:如何在同一个程序中结合C++和C

  1. C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓”兼容”,指的是编译器在”预编译器相依的特性上”一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题
  2. 有四个事情需要考虑:name mangling(名字重整)statics(静态对象)初始化动态内存分配数据结构的兼容性
Name Mangling(名字重整)
  1. Name mangling是C++用于支持函数重载的机制,它对函数名称进行一定的修正,使得每个函数有独一无二的名称,但是C不支持函数重载。当C++企图调用C中的某个函数时,会因为找不到重整的函数名而报错。因此要压抑name mangling,必须使用C++的extern “C”指令。
    1
    2
    extern "C"
    void drawLine(int x1, int y1, int x2, in y2);
    技术上说,extern “C”意味着这个函数有C linkage。如果你写了供给其他语言的程序,那么也可以使用extern “C”,只要压抑了C++函数名称的name mangling程序,就可以了。
    extern “C”也可以施行于整一组函数身上:
    1
    2
    3
    4
    5
    6
    7
    // 预处理器符号__cplusplus只针对C++才有定义,可以选择来进行C/C++语言间的判断
    extern "C" {
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
    ...
    }
Statics的初始化
  1. 程序的入口实际上并不是main,而是编译器提供的特殊函数(如 mainCRTStartup(void)等,在mainCRTStartup主要任务之一就是全局对象(包括static class对象,全局对象,namespace作用域内的对象以及文件作用域内的对象)的初始化工作,由于C++支持动态初始化(如全局变量int b=a;)而C仅支持静态初始化,因此全局对象的动态初始化涉及到动态初始化就应该在C++中撰写main,而将C main重命名为realMain,然后让C++ main调用realMain,像这样:
    1
    2
    3
    4
    5
    6
    extern "C" // implement this
    int realMain(int argc, char *argv[]); // C程序库的函数,用于完成主函数功能
    int main(int argc, char *argv[]) // C++代码
    {
    return realMain(argc, argv);
    }
动态内存分配
  1. 动态内存分配规则很简单:C++部分使用new和delete,C部分使用malloc(及其变种)和free。new分配就delete删除,malloc分配就free释放。
数据结构的兼容性
  1. C和C++对于struct和内置类型变量是兼容的,因为C/C++对于struct的内存布局相同,但如果C++为struct加上非虚成员函数,由于struct内存布局不改变,其对象仍兼容于C,但如果加上虚函数,由于vtbl和vptr的存在,struct内存布局便发生改变,也就不再兼容于C。

    也就是说,在C和C++之间对数据结构做双向交流是安全的——前提是结构的定义式在C和C++中都可编译;如果为C++ struct加上非虚函数,虽然不再兼容于C,但可能不影响兼容性;其他改变如虚函数和继承等则会产生影响。

条款35:让自己习惯于标准C++语言

  1. 在从1990年发行之后,The Annotated C++ Reference ManualARM)成了程序员的参考凭借。直到ISO/ANSI委员会对这个语言的标准化工作,最后这个参考的标准,ARM不再适合。
  2. C++ ISO/ANSI标准规格,是编译器厂商的咨询对象,也是人们遇到问题与纠纷时的手边依据和最后仲裁。ARM出版后的这些年里,C++最重要几项改变如下:
    • 增加了一些新的语言特性:RTTI、namespaces、bool、关键字mutable和explicit、enums作为重载函数参数所引发的类型晋升转换,以及在class定义区内直接为整数型const static class members设定初值的能力。
    • 扩充了Templates的弹性:允许member tempates存在、接纳”明白指示template当场实例化“的标准语法、允许function templates接受非类型自变量(non-type arguments)、可用class templates作为其他template的自变量。
    • 强化了异常处理机制(Exception handing):编译期更严密检验exception specifications、允许unexpected函数抛出bad-exception对象。
    • 修改了内存分配例程:加入operator new[]和operator delete[],内存未能分配成功时由operators new/new[]抛出一个exception,提供一个operators new/new[]新版本,在内存分配失败时返回0.
    • 增加了新的转型形式:static_cast,dynamic_cast,const_cast和reinterpret_cast。
    • 语言规则更为优雅精炼:重新定义虚函数时返回类型不再一定得与原定义完全符合,临时对象的寿命也有了明确规范。
  3. 描述STL之前,有两个C++标准程序库特质需要知道:一是标准程序库中的每样东西几乎都是template;二是所有成分都位于namespace std内。
Standard Template Library(STL)
  1. STL不难理解,它以3个基本概念为基础:containersiteratorsalgorithms。Containers持有一系列对象。Iterators是一种类似指针的对象,用以遍历STL containers,Algorithms可用于STL containers身上的函数,以iterators来协助工作。
  2. STL是可扩充的,只要遵循STL的标准,可以将自己的容器,迭代器,算法等结合STL使用。(要使自定义的迭代器适用于STL的泛型算法,需要了解C++的traits技法,见Effective C++ 条款47)

设计C++软件时,有一些问题会不断重复出现。例如,如何让constructors及nonmember
functions像虚函数一样地作用?如何限制class实体个数?如何阻止对象产生于heap内?如何保证对象产生于heap内?如何能够产生某种对象,使它再某些class的member
functions被调用时,自动执行某些动作?如何令不同的对象共享同一份数据结构,却让用户错以为每个对象各自有一份数据?如何区分operator[]的读写用途?如何产生一个虚函数,使其行为视多个对象的动态类型而定?

条款25:将constructor和non-member functions虚化

  1. 当你手上有一个对象的pointer或reference,而你不知道该对象的真正类型是什么的时候,你会调用virtual function虚函数)以完成因类型而异的行为。当你尚未获得对象,但已经知道需要什么类型的时候,你会调用constructor以构造对象。那么virtual constructors是什么?假如设计一个软件,用来处理时事新闻,内容由文字和图形构成。组织如下:
    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 抽象基类,时事消息的组件(components),至少一个纯虚函数
    class NLComponent
    {
    public:
    virtual NLComponent* clone() const = 0;
    ...
    };

    class TextBlock : public NLComponent
    {
    public:
    virtual TextBlock* clone() const
    {
    return new TextBlock(*this);
    }
    ...
    };

    class Graphic : public NLComponent
    {
    public:
    virtual Graphic* clone() const
    {
    return new Graphic(*this);
    }
    ...
    };

    class NewsLetter
    {
    public:
    NewsLetter(istream &str);
    NewsLetter(const NewsLetter &rhs);
    ...
    private:
    list<NLComponent*> components;
    // 从str读取下一个NLComponent的数据,产生组件并返回一个指向它的指针
    static NLComponent* readComponent(istream &str);
    };

    NewsLetter::NewsLetter(istream &str)
    {
    while (str)
    {
    components.push_back(readComponent(str));
    }
    }

    NewsLetter::NewsLetter(const NewsLetter &rhs)
    {
    // 迭代遍历rhs的list,运用每个元素的virtual copy constructor,将元素复制
    for (list<NLComponent*>::const_iteraotr it = rhs.components.begin();
    it != rhs.components.end();
    ++it)
    // 调用it当前指向rhs.components的元素,然后调用该元素的clone取得副本加到本对象的list尾端
    components.push_back((*it)->clone());
    }
    readComponent产生了一个崭新的对象,是TextBlock是Graphic视读入的数据而定。由于它产生了新对象,行为仿若constructor,但它能产生不同类型的对象,所以它是一个virtual constructor所谓virtual constructor是某种函数,它获得输入,可产生不同类型的对象
  2. 有一种特别的virtual constructor——所谓virtual copy constructor。它返回一个指针,指向其调用者的一个新副本。virtual copy constructors通常以copySelf或cloneSelf命名:
    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
    class NLComponent
    {
    public:
    virtual NLComponent* clone() const = 0;
    ...
    };

    class TextBlock : public NLCommponent
    {
    public:
    // virtual copy constructor
    virtual TextBlock* clone() const
    {
    return new TextBlock(*this);
    }
    ...
    };

    class Graphic : public NLComponent
    {
    public:
    // virtual copy constructor
    virtual Graphic* clone() const
    {
    return new Graphic(*this);
    }
    ...
    };
    class的virtual copy constructor只是调用真正的copy constructor而已。真正的copy constructor执行的是浅拷贝shallow copy),virtual copy constructor一样,如果真正的copy constructor执行的是深复制deep copy),virtual copy constructor亦然。
    上述实现手法是利用“虚函数之返回类型”规则中的一个宽松点,它是晚些才被接纳的规则。当derived class重新定义其base class的虚函数时,不再需要得声明与其原本相同的返回类型
    将Non-Member Functions的行为虚化
  3. 像constructors无法真正被虚化一样,non-member functions也是。让output操作符虚化(operator<<),获得一个ostream&作为其左端自变量,因此它不可能成为member function(其实可以,但会发生):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class NLComponent
    {
    public:
    virtual ostream& operator<<(ostream &str) const = 0;
    ...
    };

    class TextBlock : public NLComponent
    {
    public:
    virtual ostream& operator<<(ostream &str) const;
    };

    class Graphic : public NLComponent
    {
    public:
    virtual ostream& operator<<(ostream &str) const;
    };
    TextBlock t;
    Graphic g;
    ...
    // 此些语法与传统不符,Clients必须把stream对象放在<<左侧
    t << cout;
    g << cout;
    如果我们用虚函数(如print)来作为打印,就跟其他类型对象语法不一致了,很不自然。我们需要的是名为operator<<的non-member function
    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
    class NLComponent
    {
    public:
    virtual ostream& print(ostream &s) const = 0;
    ...
    };

    class TextBlock : public NLComponent
    {
    public:
    virtual ostream& print(ostream &s) const;
    ...
    };

    class Graphic : public NLComponent
    {
    public:
    virtual ostream& print(ostream &s) const;
    ...
    };

    inline ostream& operator<<(ostream &s, const NLComponent &s)
    {
    return c.print(s);
    }

条款26:限制某个class所能产生的对象数量

允许零个或一个对象
  1. 没当产生一个对象,会有constructor被调用。阻止某个对象被产出的方法是将constructor声明为private:
    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
    class PrintJob;
    class Printer
    {
    public:
    void submitJob(const PrintJob &job);
    void reset();
    void performSelfTest();
    ...
    friend Printer& thePrinter();
    private:
    Printer();
    Printer(const Printer &rhs);
    ...
    };

    Printer& thePrinter()
    {
    static Printer p;
    return p;
    }

    class PrintJob
    {
    public:
    PrintJob(const string &whatToPrint);
    ...
    };
    string buffer;
    ..
    thePrinter().reset();
    thePrinter().submitJob(buffer);
    这个设计由三个成分。第一,Printer class的constructor的属性为private,可以压制对象诞生;第二,全局函数thePrinter被声明为class的一个friend,使thePrinter不受private constructors的约束;第三,thePrinter内含一个static Printer对象,意指只有一个Printer对象被产出。
    在此thePrinter的实现代码中,有两个精细的地方值得探讨。第一个细微点是,形成唯一的Printer对象,是函数中的static对象而非class中的staic对象。“class拥有一个static对象意思是:即使从未被用过,它也会被构造(及析构)。相反”函数拥有一个static对象“指的是,此对象在函数第一次被调用才产生。让打印机成为class static而非一个function static有个缺点,那就是不知道它的初始化时机,而function static的初始化实际是确切知道的。
    第二个细微点函数的”static对象与inlining的互动“。这个函数未被声明为inline。因为声明static意味着只需要唯一一个对象,但对于inline non-member function其中内含local static对象,意味着你这个函数有内部连接(internal linkage)。你的程序可能会拥有多份该static对象的副本。(新版编译期这个问题已经消除)
不同的对象构造状态
  1. 或许有人认为使用计数器来限制对象数量更简单。甚至是更一般化,可以使对象的最大数量可以设定为1以外的值。但这策略有问题。当其他对象继承Printer对象或者包含Printer时,就会抛出TooManyObjects exception。除非避免具体类继承其他具体类。问题出在Printer对象可在3种不同状态下生存:(1)它自己,(2)派生物的base class成分,(3)内嵌于较大对象之中。通常你只对上述(1)感兴趣。如果采用原先的策略,很容易达成。因为constructor是private的,如果没有声明任何friend的话,是不能被用来当作base classes的,也不能内嵌于其他对象内。
允许对象生生灭灭
  1. 知道了对象的constructor可于3种情况下被调用,知道了令constructors成为private可以混淆的对象计数。虽然用thePrinter函数封装起来,虽然限制了Printer对象的个数为1,但也限制了每次执行程序只能有唯一一个Printer对象(???),因此不能写出一下代码:
    1
    2
    3
    4
    5
    6
    create Printer object p1;
    use p1;
    destroy p1;
    create Printer object p2;
    use p2;
    destroy p2;
    唯一的做法是将稍早的对象计数(object-counting)码和先前所见的伪构造函数(pseudo-constructors)结合起来:
    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
    48
    49
    50
    51
    52
    53
    class Printer
    {
    public:
    class TooManyObjects {};
    // pseudo-constructor
    static Printer* makePrinter();
    static Printer* makePrinter(const Printer &rhs);
    ~Printer();
    void submitJob(const PrintJob &job);
    void reset();
    void performSelfTest();
    ...
    private:
    static size_t numObjects;
    static const size_t Printer::maxObjects = 10; //
    Printer();
    // 不允许拷贝 E27
    Printer(const Printer &rhs);
    };

    // class static
    size_t Printer::numObjects = 0;
    const size_t Printer::maxObjects;

    Printer::Printer()
    {
    if (numObjects >= maxObjects)
    throw TooManyObject();
    proceed with normal object construction here;
    ++numObjects;
    }

    PrinterPrinter(const Printer &rhs)
    {
    if (numObjects >= maxObejcts)
    throw TooManyObjects();
    ...
    }

    Printer* Printer::makePrinter()
    { return new Printer; }

    Printer* Printer::makePrinter(const Printer &rhs)
    { return new Printer(rhs); }

    // old
    Printer p1; // error, default ctor is private
    Printer *p2 = Printer::makePrinter(); // ok
    Printer p3 = *p2; // error
    p2->performSelfTest(); //
    p2->reset();
    ...
    delete p2; // 如果p2是smart pointer就不用
    这就被泛化伪任意个数(不限一个)的对象,它将原本的常量1改为class专属的一个数值。
一个用来计算对象个数的Base Class
  1. 我们可以轻易完成一个base class,用来当作对象计数来使用,并让Printer之类的classes继承它。确保计算对象的每个class都有各自的计数器。设计一个class template:
    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
    template<class BeingCounted>
    class Counted
    {
    public:
    class TooManyObject {}; // 可能抛出exceptions
    static int objectCount()
    {
    return numObjects;
    }
    protected:
    Counted();
    Counted(const Counted &rhs);
    ~Counted() { --numObjects; }
    private:
    static int numObjects;
    static const size_t maxObjects;
    void init(); // 避免ctor重复出现
    };

    template<class BeingCounted>
    Counted<BeingCounted>::Counted()
    { init(); }

    template<class BeingCounted>
    Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
    { init(); }

    template<class BeingCounted>
    void Counted<BeingCounted>::init()
    {
    if (numObjects >= maxObjects) throw TooManyObjects();
    ++numObjects;
    }
    修改Printer class,让它运用Counted template:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Printer : private Counted<Printer>
    {
    public:
    // pseudo-constructors
    static Printer* makePrinter();
    static Printer* makePrinter(const Printer &rhs);
    ~Printer();
    void submitJob(const PrintJob &job);
    void reset();
    void performSelfTest();
    ...
    using Counted<Printer>::objectCount;
    using Counted<Printer>::TooManyObjects;
    private:
    Printer();
    Printer(const Printer &rhs);ss
    };
    Counted的大部分作为都隐藏起来不让Printer的用户知道,但用户可能希望知道有多少个Printer对象存在。Counted template提供了objectCount函数,提供信息。然而该函数在Printer中变成了private访问层级,因为上面用的是private inheritance,为了恢复public访问层级,采用了using表达式。关于Counted内的statics义务性定义,只要将:
    1
    2
    template<class BeingCounted>
    int Counted<BeingCounted>::numObjects; // 定义并自动初始化为0
    放进某个Counted的某个实现文件就可以了。而maxObjects则放在Printer作者的某个实现文件中就可以了:
    1
    const size_t Counted<Printer>::maxObjects = 10;

条款27:要求(或禁止)对象产生于heap之中

要求对象产生于heap之中(Heap-Based Objects)
  1. 阻止clients不得使用new以外的方法产生对象,这很容易办到,只要让那些被隐式调用的构造动作和析构动作不合法就可以了。把constructors和destructor声明为private就太过了,那么只将destructors成为private就可以了。而用一个pseudo destructor函数来调用真正的destructor:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class UPNumber
    {
    public:
    UPNumber();
    UPNumber(int initValue);
    UPNumber(double initValue);
    UPNumber(const UPNumber &rhs);
    // pseudo destructor
    void destroy() const { delete this; }
    ...
    private:
    ~UPNumber();
    };

    UPNumber n; // error
    UPNumber *p = new UPNumber; // ok
    ...
    delete p; // error
    p->destroy(); // ok
    把constructors声明为private的缺点是,必须记住每一个constructor都声明为private。只要限制destructor或constructors的运用,就可以阻止non-heap objects的诞生,但是,它妨碍了继承(inheritance)内含(containment)。这个困难可以克服,只需要将UPNumber的destructor成为protected就可以解决:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class UPNumber { ... };
    class NonNegativeUPNumber : public UPNumber { ... };
    class Asset
    {
    public:
    Asset(int initValue);
    ~Asset();
    ...
    private:
    UPNumber *value;
    };

    Asset::Asset(int initValue) : value(new UPNumber(initValue)) { ... }

    Asset::~Asset()
    { value->destroy(); }
    判断某个对象是否位于heap内
  2. 没有办法侦测出一个construcotr是不是在heap内。或许可以通过设置标志位,但对于operator new[]来说,设置标志位却只有一次机会设立标志信息,而第二次就会exception。或者是利用系统的一个特点:stack(栈)高地址往低地址成长,heap(堆)由低地址往高地址成长。但是这是不可移植的。这个函数的观念很有趣。一个临时stack是个局部变量,它会被放在stack的顶端,而stack向低地址成长,所以这个stack的地址一定比任何一个位于stack中的变量(或对象)更低。因此,如果一个位于heap的对象,其地址一定比临时stack的地址更低。但这个想法不完善,很多系统把static对象放置在heap的底下。
    如果真实目的只是为了想知道对象调用delete是否安全,那就跟判断对象是否在heap是两回事了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void* operator new(size_t size)
    {
    void *p = getMemory(size);
    // add p to the collection of allocated addresses;
    return p;
    }
    void operator delete(void *ptr)
    {
    releaseMemory(ptr); // free store
    // remove ptr from the collection of allocated addresses;
    }
    bool isSafeToDelete(const void *address)
    {
    // return whether address is in collection of allocated addresses
    }
    operator new负责把一些条目(entires)加到一个由“动态分配而得的地址”所形成的集合中,operator delete负责把这些条目移除;isSafeToDelete负责查找集合,看看地址是否在其中。这些operator new和operator delete函数都在全局范围内,这应该堆所有类型都管用(甚至内置类型)。但这不是我们想要的——当对象存在多重继承或虚拟继承时,可能拥有多地址,这就是非自然多态unnatural polymorphism)。
    我们需要的是一个提供函数机能,但不符带全局命名空间的污染问题、额外的义务性负担,以及正确性的疑惑。那么abstract mixin base class抽象混合式基类)可以满足需求。抽象基类不能被实例化,因为它至少有一个纯虚函数。mix in class则提供了一组定义好的能力,能与derived 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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    class HeapTracked
    {
    public:
    class MissingAddress(); // exception class
    virtual ~HeapTracked() = 0;
    static void* operator new(size_t size);
    static void operator delete(void *ptr);
    bool isOnHeap() const;
    private:
    typedef const void *RawAddress;
    static list<RawAddress> addresses;
    };

    // static class member的义务性定义
    list<RawAddress> HeapTracked::addresses;

    HeapTracked::~HeapTracked() {}

    void* HeapTracked::operator new(size_t size)
    {
    void *memPtr = ::operator new(size); // 取得内存
    addresses.push_front(memPtr); // 将地址放到list头部
    return memPtr;
    }

    void HeapTracked::operator delete(void *ptr);
    {
    list<RawAddress>::iterator it = find(address.begin(), address.end(), ptr);
    if (it != address.end()) {
    addresses.erase(it);
    ::operator delete(ptr);
    } else throw MissingAddress();
    }

    bool HeapTracked::isOnHeap() const
    {
    const void *rawAddress = dynamic_cast<const void*>(this);
    list<RawAddress>::iterator it = find(address.begin(), address.end(), rawAddress);
    return it != addresses.end();
    }

    class Asset : public HeapTracked
    {
    private:
    UPNumber value;
    ...
    };

    void inventoryAsset(const Asset *ap)
    {
    if (ap->isOnHeap())
    ap is a heap-based asset -- inventory it as such;
    else
    ssap is a non-heap-based asset -- record it that way;
    }
    凡涉及“多重或虚拟基类”的对象,会拥有多个地址,如果写在全局函数就会很复杂,但是isOnHeap只施行于HeapTracked对象身上,所以只要简单的将指针dynamic_cast为**void*(或const void*volatile void*const volatile void***),就会获得一个指针,指向原指针所指对象的内存起始处。不过dynamic_cast只适用于有至少一个虚函数的指针身上。之所以前面的isSafeToDelete撰写很复杂,就是因为它可以对任何类型起作用,因此dynamic_cast无法帮助它。isOnHeap有所选择(只针对HeapTracked对象的指针)。这个技术有移植性,只要编译器支持dynamic_cast。
禁止对象产生于heap之中
  1. 一般有3种情况:
    • 对象被直接实例化
    • 对象被实例化为derived class objects内的base class成分
    • 对象被内嵌于其他对象之中。
      阻止clients直接将对象实例化于heap之中,很容易,只需要将operator new和operator delete声明为private就可以了,如果想禁止对象所组成的数组,可以将operator new[]和operator delete声明为private。

条款28:Smart Pointers(智能指针)

  1. Smart poinnters是一种像内建指针,却提供了更多机能的对象。当以smart pointers取代C++的内建指针(dumb pointer),你会获得各种指针行为的控制权:
    • 构造和析构Construction and Destruction)。你可以决定smart pointer被产生以及被构造时发生什么事。通常给smart pointers一个默认值0,以避免指针未获初始化。
    • 复制和复制Copying and Assignment)。当一个smart pointer被复制或涉及复制动作时,可以控制发生什么事。
    • 解引Dereferencing)。当client解引(取用)smart pointer所指的对象时,有权决定发生什么事情。
      Smart pointers由templates产生出来。由于像内建指针一样,所以它必须由强烈的类型性strongly teyped)。
Smart Pointers的构造、复制、析构
  1. auto_ptr template可能实现如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<class T>
    class auto_ptr
    {
    public:
    auto_tpr(T *ptr = 0) : pointee(ptr) {}
    ~auto_ptr() { delete pointee; }
    ...
    private:
    T *pointee;
    };
    在同一个对象只可被一个auto_ptr拥有的前提下,上述做法有效运转。但一旦auto_ptr被复制或被赋值,会发生什么?会导致两个auto_ptrs指向同一对象。这当在销毁对象时,可能会删除两次,往往会导致未定义。
    另一个做法是以new操作符为所指对象产生一个新副本。而auto_ptr不得指向一个类型为T的对象,可以指向一个T派生类型的对象。auto_ptr采用了一个富弹性的解法:当auto_ptr被复制或被赋值,其对象拥有权会转移:
    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
    template<class T>
    class auto_ptr
    {
    public:
    ...
    auto_ptr(auto_ptr<T> &rhs); // copy constructor
    auto_ptr<T>& operator=(auto_ptr<T> &rhs); // assignment operator
    ...
    };

    template<class T>
    auto_ptr<T>::auto_ptr(auto_ptr<T> &rhs);
    {
    pointee = rhs.pointee;
    rhs.pointee = 0;
    }

    template<class T>
    auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T> &rhs)
    {
    if (this == &rhs);
    return *this;
    delete pointee; // 删除原有的对象
    pointee = rhs.pointee;
    rhs.pointee = 0;
    return *this;
    }
    由于auto_ptr的copy constructor被调用时,对象拥有权转移了,所以以by value方式转递auto_ptrs往往是个糟糕的主意:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void printTreeNode(ostream &s, auto_ptr<TreeNode> p)
    { s << *p; }

    int main()
    {
    auto_ptr<TreeNode> ptn(ew TreeNode);
    ...
    printTreeNode(cout, ptn); // 以by value传递
    }
    当printTreeNode的参数被p初始化(通过auto_ptr的copy constructor),ptn所指的对象拥有权被转移至p。当printTreeNode结束,p离开生存空间,destructor被调用。然而ptn不再指向任何东西,这将产生未定义行为。所以Pass-by-reference-to-const才是适当的途径:
    1
    2
    void printTreeNode(ostream &s, const auto_ptr<TreeNode> &p)
    { s << *p; }
    在此函数中,p是个referene而不是对象。所以不会由constructor被用来为p设定初值,ptn将保留拥有权。
实现Dereferencing Operators(解引操作符)
  1. 现在注意力放在smart pointers的核心:operator*和operator->函数身上:
    1
    2
    3
    4
    5
    6
    template<class T>
    T& SmartPtr<T>::operator*() const
    {
    perform "smart pointer" processing;
    return *pointee;
    }
    这个函数首先做任何必要的初始化动作或是让pointee获得有效值的任何动作。
    检验operator->前,看一下此函数的不寻常意义:
    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
    // 产生smart ptrs,用来指向分布式数据库(DB)内的对象
    template<class T>
    class DBPtr
    {
    public:
    DBPtr(T *realPtr = 0);
    DBPtr(DataBaseID id);
    ...
    };

    // 用来表现数据库中的一笔数据(tuples)
    class Tuple
    {
    public:
    ...
    void displayEditDialog(); // 呈现图形式对话框,供输入tuple
    bool isValid() const; // 检验*this是否有效
    };

    // class template,用来在T对象被修改时,完成运转记录(log entires)。
    template<classs T>
    class LogEntry
    {
    public:
    LogEntry(const T &objectToBeModified);
    ~LogEntry();
    };

    void editTuple(DBPtr<Tuple &pt)
    {
    LogEntry<Tuple> entry(*pt);
    // 反复显示编辑对话框,知道获得有效值为止
    do pt->displayEditDialog();
    while (pt->isValid() == false);
    }
    其中pt->displayEditDialog();会被编译器解释为:(pt.operator->())->displayEditDialog();这个意味着,不论operator->返回什么,在该回传值身上施行->操作符都是合法的。因此operator->只能返回两个东西:一个dumb pointer或是一个smart pointer:
    1
    2
    3
    4
    5
    6
    template<class T>
    T* SmartPtr<T>::operator->() const
    {
    perform "smart pointer" processing;
    return pointee;
    }
测试Smart Pointers是否为NULL
  1. 我们可以产生、销毁、赋值、复制、解引smart pointers。但有一件事没办法做,就是判断是否为NULL:
    1
    2
    3
    4
    5
    SmartPtr<TreeNode> ptn;
    ...
    if (ptn == 0) ... // error
    if (ptn) ... // error
    if (!ptn) ... // error
    为我们smart pointer class添加一个isNULL很容易,但不自然。另一个做法是,提供一个隐式转换操作符,允许上述测试动作通过编译。这个转换的传统目标是void*
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<class T>
    class SmartPtr
    {
    public:
    ...
    operator void*(); // 如果dumb ptr是null,返回0,否则是非0值
    ...
    };

    SmartPtr<TreeNode> ptn;
    ...
    if (ptn == 0) ... // ok
    if (ptn) ... // ok
    if (!ptn) ... // ok
    这个做法的缺点是,它竟然可以用来对完全不同类型的对象比较(我好像没出现这样的情况……)!这个问题无法解决,但有个差强人意的做法,允许提供测试nullness的合理语法,并能够将意外引起不同类型之smart pointers相互比较的机会降到最低,那就是将!操作符重载,并在其调用者是null的情况下,返回true:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    template<class T>
    class SmartPtr
    {
    public:
    ...
    bool operator!() const;
    ...
    };

    SmartPtr<TreeNode> ptn;
    ...
    if (!ptn) ... // ok ptn is null
    else ... // ptn is not null

    if (ptn == 0) ... // error
    if (ptn) ... // error

    SmartPtr<Apple> pa;
    SmartPtr<Orange> po;
    ...
    if (!pa == !po) ... // ok
    这个有风险的做法的原因是很少有程序员这么个写法。在C++标准程序库中,隐式转换为void*已被隐式转换为bool取代,而operator bool总是返回operator!的反相。
将Smart Pointers转换为Dumb Pointers
  1. 有时候你希望将smart pointers加入已使用的dumb pointers应用软件中,
    1
    2
    3
    4
    5
    6
    7
    class Tuple { ... };
    void normalize(Tuple *pt); // by dumb pointer

    DBPtr<Tuple> pt;
    ...
    normaize(pt); // error
    normalize(&*pt);// ok
    那个转换动作很难看,如果为smart pointer-to-T template加上隐式类型转换操作符,使之转换为dumb pointer-to-T,先前的调用就能成功。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<class T>
    class DBPtr
    {
    public:
    ...
    operator T*() { return pointee; }
    ...
    };

    DBPtr<Tuple> pt;
    ...
    normalize(pt); // ok
    上述函数一加上,nullness测试问题也一并解决了。不过这样的转换也有阴暗面,它式clients得以轻易地直接对dumb pointers做动作,因而回避了smart pointer的最初目的。允许直接适用dumb pointers有灾难,它导致class的计数簿记工作方面的错误。造成引用计数所用的数据结构崩溃。
    即使提供了一个隐式转换操作符,smart pointer还是无法完全取代dumb pointer。因为从smart pointer转换为dumb pointer是一种用户定制的转换行为,而编译器禁止一次施行一次以上这类转换。举个例子,有个class,考虑一个函数用来整合两个TupleAccessors对象的信息:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class TupleAccessors
    {
    public:
    TupleAccessors(const Tuple *pt);
    ...
    };

    TupleAccessors merge(const TupleAccessors &ta1, const TupleAccessors &ta2);

    Tuple *pt1, *pt2;
    merge(pt1, pt2); // ok

    DBPtr<Tuple> pt1, pt2;
    merge(pt1, pt2); // error
    这是因为从DBPtr转换为TupleAccessors需要两个用户定制转换,而着C++不允许,所以第一个将DBPtr转换为Tuple*,而第二个将需要将Tuple*转环为TupleAccessors)。所以不要提供对dumb pointers的隐式转换操作符,除非不得已。
Smart Pointers和与继承有关的类型转换
  1. 假设有一个public inheritance继承体系,构建出消费性音乐产品:
    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
    class MusicProduct
    {
    public:
    MusicProduct(const string &title);
    virtual void play() const = 0;
    virtual void displayTitle() const = 0;
    ...
    };

    class Cassette : public MusicProduct
    {
    public:
    Cassette(const string &title);
    virtual void play() const;
    virtual void displayTitle() const;
    ...
    };

    class CD : public MusicProduct
    {
    public:
    CD(const string &title);
    virtual void play() const;
    virtual void displalyTitle() const;
    ...
    };

    void displayAndPlay(const MusicProduct *pmp, int numTimes);
    {
    for (int i = 1; i <= numTimes; ++i)
    {
    pmp->displayTitle();
    pmp->play();
    }
    }

    // dumb pointers的方式没问题,smart pointer取代之则另一种情况了
    void displayAndPlay(const SmartPtr<MusicProduct> &pmp, int numTimes);
    SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
    SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));
    displayAndPlay(funMusic, 10); // error
    displayAndPlay(nightmareMusic, 0); // error
    之所以无法通过编译,是因为编译器所看见的是3个互不相干的classs。有个办法可以绕弯解除这一束缚:令每个smart pointer class有个隐式类型转换操作符,用来转换至另一个smart pointer class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class SmartPtr<Cassette>
    {
    public:
    operator SmartPtr<MusicProduct>()
    { return SmartPtr<MusicProduct>(pointee); }
    ...
    private:
    Cassette *pointee;
    };

    class SmartPtr<CD>
    {
    public:
    operator SmartPtr<MusicProduct>()
    { return SmartPtr<MusicProduct>(pointee); }
    ...
    private:
    CD *pointee;
    }
    这个做法有两个缺点。第一,必须为每一个“SmartPtr class实例”加入上述例子;第二,需要加上很多这样的转换操作符,因为所指的对象位于继承体系的底层,必须为对象直接继承或间接继承的每个base class提供一个转换函数。由于编译器禁止一次执行一个以上的用户定制类型转换函数,所以无法将smart pointer-to-T转换为一个smart pointer-to-indirect-base-class-of-T。刚好有个语言扩充性质,它可以将nonvirtual member function声明为template,可以用它来产生smart pointer的转换函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // template class,用于smart pointer-to-T对象
    template<class T>
    class SmartPtr
    {
    public:
    SmartPtr(T *realPtr = 0);
    T* operator->() const;
    T& operator*() const;
    // template function,用于隐式转换操作符
    template<class newType>
    operator SmartPtr<newType>()
    { return SmartPtr<newType>(pointee); }
    ,,,
    };
    funMusic对象属于SmartPtr类型,而displayAndPlay函数期望得到一个SmartPtr对象。编译器发现类型不吻合,于是企图将funMusic转换其期望的对象。编译器在SmartPtr class内企图找一个”单一变量之construcotr“,其自变量类型为SmartPtr,但没有找到;于是企图在SmartPtr class内找一个隐式类型转换操作符,希望产出已给SmartPtr class,但也失败了;接下来再试图寻找一个”可实例化以导出合宜转换函数“的member function template。它在SmartPtr找到一个东西,当它被实例化并令newType绑定MusicProduct时,编译器将之实例化,可得:
    1
    2
    SmartPtr<Cassette>::operator SmartPtr<MusicProduct>() 
    { return SmartPTr<MusicProduct>(pointee); }
    这其中涵盖的技术不简单,它包括:(1)函数调用的自变量匹配规则、(2)隐式类型转换函数、(3)template functions的暗自实例化、(4)member function templates等技术。
Smart Pointers与const
  1. const可以修饰被指的东西
    1
    2
    3
    4
    CD goodCD("Flood");
    const CD *p; // p时non-const指针,指向const CD object
    CD *const p = &goodCD; // p是哟个const指针,指向non-const CD object,必须有初值
    const CD *const p = &goodCD; //p是一个const指针,指向一个const CD object
    很自然,我们想要smart pointers也有同样的弹性。但Smart pointer只能有一个地方放置const,只能施行与指针身上,不能及于所指对象,不过我们可以对const以及non-const的对象及指针,产生4种组合:
    1
    2
    3
    4
    SmartPtr<CD> p;
    Smart<const CD> p;
    const SmartPtr<CD> p = &goodCD;
    const SmartPtr<const CD> p = &goodCD;
    如果适用dumb pionters,可以non-const指针作为const指针的初值,也可以指向non-const独享的指针作为指向const对象指针的初始值,赋值规则类似:
    1
    2
    3
    4
    CD *pCD = new CD("Famous Movie Themes");
    const CD *pConstCD = pCD; // ok
    SmartPtr<CD> pCD = new CD("Famous Movie Themes");
    SmartPtr<const CD> pConstCD = pCD; // error
    类型转换如果涉及const,便是一条单行道:从non-const到const是安全的,从const到non-const是不安全的。解决上面的方法是,将derived class object转换为base class object,令smart pointer-to-T class公开继承一个对应的smart pointer-to-const-T class:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    template<class T>
    class SmartPtrToConst
    {
    ...
    functions
    protected:
    union(
    {
    const T *constPointee;
    T *pointee;
    };
    };

    template<class T>
    class SmartPtr : public SmartPtrToConst<T>
    {
    ...
    };
    运用这个设计,我们获得了自己希望的行为:
    1
    2
    SmartPtr<CD> pCD = new CD("Famous Movie Themes");
    SmartPtrToConst<CD> pConstCD = pCD; // ok

条款29:Reference counting(引用计数)

  1. Reference counting这项技术,允许多个等值对象共享同一个实值。Reference counting可以消除记录对象拥有权的负荷,因为当对象用reference counting技术,它拥有它自己,一旦没人使用它,便自动销毁自己。因此,reference counting建构出垃圾回收机制garbage colection)的一个简单形式。
Reference Counting(引用计数)的实现
  1. 基本设计像这样:
    1
    2
    3
    4
    5
    6
    7
    class String {
    public:
    ...
    private:
    struct StringValue { ... }; // 包含引用计数和字符串值
    StringValue *value; // value of this String
    };
    内嵌的结构体StringValue主要用于存储引用计数和字符串值,并使得引用计数和字符串值相关联.StringValue的实现像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class String {
    private:
    struct StringValue {
    int refCount;
    char *data;
    StringValue(const char *initValue);
    ~StringValue();
    };
    ...
    };
    String::StringValue::StringValue(const char *initValue) : refCount(1)
    {
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
    }
    String::StringValue::~StringValue()
    {
    delete [] data;
    }
    StringValue只对String类可见,而对客户不可见,接口由String定义并提供给客户。String的构造函数:
    1
    2
    String(const char *initValue = "");
    String(const String& rhs);
    第一个构造函数的实现较简单,根据传入的char数组构造StringValue对象,然后使String中的指针指向这个String即可:
    1
    String::String(const char *initValue): value(new StringValue(initValue)){}
    但这样的实现导致”分开构造,但拥有相同初值的String对象,并不共享同一个数据结构”,因此像这样的代码:
    1
    2
    String s1("More Effective C++");
    String s2("More Effective C++");
    尽管是s1和s2的值相同,但它们却并不共享同一个块内存,而是各自拥有独立内存。
    拷贝构造函数可以使用引用计数,并共享内存,像这样:
    1
    String::String(const String& rhs): value(rhs.value){ ++value->refCount; }
    析构函数负责在引用计数为0的时候撤销内存:
    1
    2
    3
    4
    5
    String::~String()
    {
    if (--value->refCount == 0)
    delete value;
    }
    赋值操作符要注意自身赋值的情况:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    String& String::operator=(const String& rhs)
    {
    if (value == rhs.value) { //处理自身赋值
    return *this;
    }
    if (--value->refCount == 0) {
    delete value;
    }
    value = rhs.value;
    ++value->refCount;
    return *this;
    }
Copy-on-Write(写时才复制)
  1. 对operator[]的重载比较复杂:const版本是只读动作,因而只返回指定字符即可,像这样:
    1
    const char& String::operator[](int index) const{ return value->data[index]; }
    但non-const版本面临着被写入新的值的可能,由于对当前String的修改不应影响到共享内存的其他String对象,因此需要先为当前String分配独立内存并将原值进行拷贝,像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    char& String::operator[](int index)
    {
    if (value->refCount > 1) {
    --value->refCount;
    value =new StringValue(value->data);
    }
    return value->data[index];
    }
    不仅是operator[],其他可能改变String对象的操作也应该采取和non-cons版本operator[]相同的动作.这其实是lazy evaluation的一种应用.
Pointers,References,以及Copy-on-Write
  1. 3中对operator[]的重载解释并解决了可能的写操作篡改共享内存的问题,但是却无法阻止外部指针或引用对共享内存的篡改,像这样:
    1
    2
    3
    String s1 = "Hello";
    char *p = &s1[1];
    String s2 = s1;
    对p所指向的内存的任何写操作都会同时更改s1和s2的值,但是s1却对此一无所知,因为p和s1没有内在联系.解决办法并不难:为每一个StringValue对象加一个标志(flag)变量,用表示是否可以被共享,开始时先将flag设为true(可以共享),一旦non-const operator[]被调用就将该flag设为false,并可能永远不许再更改(除非重新为StringValue分配更大内存而导致指针失效),StringValue的修改版像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class String {
    private:
    struct StringValue {
    int refCount;
    bool shareable;
    char *data;
    StringValue(const char *initValue);
    ~StringValue();
    };
    ...
    };
    String::StringValue::StringValue(const char *initValue): refCount(1),shareable(true)
    {
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
    }
    String::StringValue::~StringValue()
    {
    delete [] data;
    }
    String的member function在企图使用共享内存前,就必须测试内存是否允许被共享:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String::String(const String& rhs)
    {
    if (rhs.value->shareable) {
    value = rhs.value;
    ++value->refCount;
    }
    else {
    value = new StringValue(rhs.value->data);
    }
    }
    其他返回引用的member function(对于String只有operator[])都涉及到对flag的修改,而其他可能需要共享内存的member function都涉及到对flag的检测。 条款30的proxy class技术可以将operator[]的读和写用途加以区分,从而降低”需被标记为不可共享”之StringValue对象的个数。
一个Reference-Counting(引用计数)基类
  1. 任何要支持内存共享的class都可以使用reference-counting,因此可以考虑把它抽象为一个类,任何需要reference-counting功能的class只要使用这个类即可。

    第一步就是产生一个base class RCObject,执行引用计数的功能并标记对象是否可被共享,像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class RCObject {
    public:
    RCObject();
    RCObject(const RCObject& rhs);
    RCObject& operator=(const RCObject& rhs);
    virtual ~RCObject() = 0;
    void addReference();
    void removeReference();
    void markUnshareable();
    bool isShareable() const;
    bool isShared() const;
    private:
    int refCount;
    bool shareable;
    };

    由于RCObject的作用只是实现引用计数的辅助功能,然后让StringValue继承它,因此StringValue被设为一个抽象基类——通过将析构函数设为纯虚函数,但仍需要为析构函数提供定义.RCObject的实现像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    RCObject::RCObject(): refCount(0), shareable(true) {}
    RCObject::RCObject(const RCObject&):refCount(0),shareable(true) {}
    RCObject& RCObject::operator=(const RCObject&)
    { return *this; }
    RCObject::~RCObject() {} // virtual dtors must always
    void RCObject::addReference() { ++refCount; }
    void RCObject::removeReference()
    { if (--refCount == 0) delete this; }
    void RCObject::markUnshareable()
    { shareable = false; }
    bool RCObject::isShareable() const
    { return shareable; }
    bool RCObject::isShared() const
    { return refCount > 1; }

    RCObject的实现非常简单,但是其拷贝构造函数和赋值操作符有些特殊——它们的参数没有名字,也就是说参数没有作用,其拷贝构造函数和赋值操作符都只是形式上的:

    RCObjetc拷贝构造函数与RCObject的作用相对应——RCObject一旦被构造,就说明一个新的对象被产生出来,那么RCObject对象本身的初始值和默认构造函数相同,至于refCount设为0而不是1,这要求对象创建者自行将refCount设为1.

    RCObject的赋值操作符什么也不做,仅仅返回*this,因为它不应该被调用,正如之前的StringValue,如果对String对象赋值,那么或者StringValue被共享,或者拷贝构造一个新的StringValue,实际上StringValue的赋值操作永远不会被调用.即使要对StringValue做赋值操作,像这样:

    1
    sv1=sv2;//sv1和sv2是StringValue型对象

    指向sv1和sv2的对象数目实际上并未改变,因此sv1的基类部分RCObject什么也不做仍然是正确的.
    removeReference的责任不仅在于将refCount减1,实际上还承担了析构函数的作用——在refCount=1的时候delete销毁对象,从这里可以看出RCObject必须被产生于heap中.
    StringValue要直接使用RCObject,像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class String {
    private:
    struct StringValue: public RCObject {
    char *data;
    StringValue(const char *initValue);
    ~StringValue();
    };
    ...
    };
    String::StringValue::StringValue(const char *initValue)
    {
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
    }
    String::StringValue::~StringValue()
    {
    delete [] data;
    }

    StringValue类public继承自RCObject,因此它继承了RCObject的接口并供String使用,StringValue也必须构造在heap中.

自动操作Reference Count(引用计数)
  1. RCObject提供了一定程度的代码复用功能,但还远远不够——String类仍然需要手动调用RCObject的成员函数来对引用计数进行更改.解决方法就是”计算机科学领域中大部分问题得以解决的原理”——在中间加一层,也就是在String和StringValue中间加一层智能指针类对引用计数进行管理,像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //管理引用计数的智能指针类
    template<class T>
    class RCPtr {
    public:
    RCPtr(T* realPtr = 0);
    RCPtr(const RCPtr& rhs);
    ~RCPtr();
    RCPtr& operator=(const RCPtr& rhs);
    T* operator->() const; // see Item 28
    T& operator*() const; // see Item 28
    private:
    T *pointee;
    void init(); //将构造函数中的重复操作提取成一个函数
    };
    之前RCPtr是一个类模板,String之前有一个StringValue*成员,现在只要将它替换为RCPtr即可.
    RCPtr的构造函数像这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    template<class T>
    RCPtr<T>::RCPtr(T* realPtr): pointee(realPtr)
    {
    init();
    }
    template<class T>
    RCPtr<T>::RCPtr(const RCPtr& rhs): pointee(rhs.pointee)
    {
    init();
    }
    template<class T>
    void RCPtr<T>::init()
    {
    if (pointee == 0) {
    return;
    }
    if (pointee->isShareable() == false) {
    pointee = new T(*pointee);
    }
    pointee->addReference();//引用计数的更改负担转移到这里
    }
    init中使用了new关键字,它调用T的拷贝构造函数,为防止编译器为StringValue合成的拷贝构造函数执行浅复制,需要为StringValue定义执行深度复制的拷贝构造函数,像这样:
    1
    2
    3
    4
    5
    String::StringValue::StringValue(const StringValue& rhs)
    {
    data = new char[strlen(rhs.data) + 1];
    strcpy(data, rhs.data);
    }
    此外,由于多态性的存在,尽管pointee是T*类型,但它实际可能指向T类型的派生类,在此情况下new调用的却是T的拷贝构造函数,要防止这种现象,可以使用virtual copy constructor(见条款25),这里不再讨论
把所有努力放到这里
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
template<class T>
class RCPtr
{
public:
RCPtr(T *realPtr = 0);
RCPtr(const RCPtr &rhs);
~RCPtr();
RCPtr& operator=(const RCPtr &rhs);
T* operator->() const;
T& operator*() const;
private:
T *pointee;
void init();
};

template<class T> void RCPtr<T>::init()
{
if (pointee == 0) return;
if (pointee->isShareable() == false)
pointee = new T(*pointee);
pointee->addReference();
}
template<class T> RCPtr<T>::RCPtr(T *realPtr) : pointee(realPtr) { init(); }
template<class T> RCPtr<T>::RCPtr(const RCPtr &rhs) : pointee(rhs.pointee) { init(); }
template<class T> RCPtr<T>::~RCPtr() { if (pointee) pointee->removeReference(); }
template<class T> T* RCPtr<T>::operator->() const { return pointee; }
template<class T> T& RCPtr<T>::operator*() const { return *pointee; }
template<class T> RCPtr<T>& RCPtr<T>::operator=(const RCPtr &rhs)
{
if (pointee != rhs.pointee)
{
if (pointee) pointee->removeReference();
pointee = rhs.pointee;
init();
}
return *this;
}

class RCObject
{
public:
void addReference();
void removeReference();
void markUnshareable();
bool isShareable() const;
bool isShared() const;
protected:
RCObject();
RCObject(const RCObject &rhs);
RCObject& operator=(const RCObject &rhs);
virtual ~RCObject() = 0;
private:
int refCount;
bool shareable;
};

RCObject::RCObject() : refCount(0), shareable(true) {}
RCObject::RCObject(const RCObject&) : refCount(0), shareable(true) {}
RCObject& RCObject::operator=(const RCObject&) { return *this; }
RCObject::~RCObject() {}
void RCObject::addReference() { ++refCount; }
void RCObject::removeReference() { if (--refCount == 0) delete this; }
void RCObject::markUnshareable() { shareable == false; }
bool RCObject::isShareable() const { return shareable; }
bool RCObject::isShared() const { return refCount > 1; }

class String
{
public:
String(const char *value = "");
const char& operator[](int index) const;
char& operator[](int index);
private:
struct StringValue : public RCObject
{
char *data;
StringValue(const char *initValue);
StringValue(const StringValue &rhs);
void init(const char *initValue);
~StringValue();
};
RCPtr<StringValue> value;
};

void String::StringValue::init(const char *initValue)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::StringValue(const char *initValue) { init(initValue); }
String::StringValue::StringValue(const StringValue& rhs) { init(rhs.data); }
String::StringValue::~StringValue() { delete []data; }

String::String(const char *initValue) : value(new StringValue(initValue)) {}
const char& String::operator[](int index) const { return value->data[index]; }
char& String::operator[](int index)
{
if (value->isShared())
value = new StringValue(value->data);
value->markUnshareable();
return value->data[index];
}

条款30:Proxy classes(替身类、代理类)

  1. 通过一个类对象来象征一个其他对象,常被称为proxy objects替身对象)。它允许我们完成某些十分困难或几乎不可能完成的行为。多维数组是其中之一,左值/右值的区分是其中之二,压抑隐式转换是其中之三。
  2. https://www.cnblogs.com/reasno/p/4858490.html

条款31:让函数根据一个以上的对象类型来决定如何虚化

  1. 假设设计一个游戏,根据宇宙飞船、太空站和小行星三者的共同特征,设计为以下情况:
    1
    2
    3
    4
    graph BT
    A[SpaceShip] --> S(GameObject)
    B[SpaceStation] --> S
    C[Asteroid] --> S
    整个继承体系:
    1
    2
    3
    4
    class GameObject { ... };
    class SpaceShip : public GameObject { ... };
    class SpaceStation : public GameObject { ... };
    class Asteroid : public GameObject { ... };
    不同对象相撞要有不同的规则,处理碰撞的函数声明像这样:
    1
    void checkForCollision(GameObject &object1, GameObject &object2);
    现在挑战出来了,处理他们之间的碰撞,首先得知道它们的动态类型。人们常把面向对象里虚函数的调用动作称为>message dispatch消息分派)。然而C++并不直接支持multiple dispatch。
虚函数 + RTTI(运行时期类型识别)
  1. 最一般化的double-dispatching实现方法是用if-then-elses来仿真虚函数:
    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
    class GameObject {
    public:
    virtual void collide(GameObject &otherObject) = 0;
    ...
    };

    calss SpaceShip : public GameObject {
    public:
    virtual void collide(GameObject &otherObject);
    ...
    };

    // 如果和一个未知类型对象相撞,就抛出以下exception
    class CollisionWithUnknownObject {
    public:
    CollisionWithUnknownObject(GameObject &whatWeHit);
    ...
    };

    void SpaceShip::collide(GameObject &otherObject) {
    const type_info &objectType = typeid(otherObject);
    if (objectType == typeid(SpaceShip)) {
    SpaceShip &ss = static_cast<SpaceShip&>(otherObject);
    // process a SpaceShip-SpaceShip collision;
    } else if (objectType == typeid(SpaceStation)) {
    SpaceStation &ss = static_cast<SpaceStation&>(otherObject);
    // process a SpaceShip-SpaceStation collision;
    } else if (objectType == typeid(Asteroid)) {
    Asteroid &a = static_cast<Asteroid&>(otherObject);
    // sprocess a SpaceShip-Asteroid collision;
    } else
    throw CollisionWithUnknownObject(otherObejct);
    }
    我们只需要决定碰撞双方的其中一个类型,因为另一个对象是*this,它的类型被虚函数机制决定下来了,是SpaceShip对象。
    这种以类型为行事基准的方法有个缺点:它会造成程序难以维护。这就是虚函数当初被发明出来的原因:把生产维护以类型为行事基准的函数的负担,从程序员转到编译器。但现在如果使用RTTI来实现double-dispatching,等于回到老而糟糕的年代。
只使用虚函数
  1. 看看如何只以虚函数来解决问题:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class SpaceShip;    // forward declarations
    class SpaceStation;
    class Asteroid;

    class GameObject {
    public:
    virtual void collide(GameObjet &otherObject) = 0;
    virtual void collide(SpaceShip &otherObject) = 0;
    virtual void collide(SpaceStation &otherObject) = 0;
    virtual void collide(Asteroid &otherObject) = 0;
    ...
    };

    class SpaceShip : public GameObject {
    public :
    virtual void collide(GameObjet &otherObject);
    virtual void collide(SpaceShip &otherObject);
    virtual void collide(SpaceStation &otherObject);
    virtual void collide(Asteroid &otherObject);
    ...
    };
    这种方法将double-dispatching以两个分离的虚函数调用实现出来的。这并不是递归调用。第一个虚函数调用动作针对的是接收GameObject&参数的collide函数,有点简单:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void SpaceShip::collide(GameObject &otherObject)
    {
    otherObject.collide(*this);
    }
    void SpaceShip::collide(SpaceShip &otherObject)
    {
    // process a SpaceShip-SpaceShip collision;
    }
    void SpaceShip::collide(SpaceStation &otherObject)
    {
    // processa SpaceShip-SpaceStation collision;
    }
    void SpaceShip:collide(Asteroid &otherObject)
    {
    // process SpaceShip-Asteroid collision;
    }
    但这种做法成本太高,每次修改需要在每个class中增添(E34)。但它也有好处,那就是不再因为为止类型而不得不抛出异常。
自行仿真虚函数表格(Virtual Function Tables)
  1. 回忆条款24,编译器通过vtbl直接索引取得函数指针,而不必条条框框if-then-else运算。这样一来效率也高,也可以当使用RTTI的时候隔离至一个点:vtbl的初始化处。
    先从GameObject继承体系内的函数开始:
    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    class GameObject {
    public:
    virtual void collide(GameObject &otherObject) = 0;
    ...
    };
    class SpaceShip : public GameObject {
    public:
    virtual void collide(GameObject &otherObject);
    virtual void hitSpaceShip(GameObject &spaceShip);
    virtual void hitSpaceStation(GameObject &spaceStation);
    virtual void hitAsteroid(GameObject &asteroid);
    ...
    private:
    typeef void (SpaceShip::*HitFunctionPtr)(GameObject&);
    typedef map<string, HitFunctionPtr> HitMap;
    // 产生中介函数lookup
    static hitFunctionPtr lookup(const GameObject &whaWeHit);
    static HitMap* initializeCollisionMap();
    ...
    };

    void SpaceShip::hitSpaceShip(SpaceShip &otherObject)
    {
    SpaceShip &otherShip = dynamic_cast<SpaceShip&>(spaceShip);
    // process a SpaceShip-SpaceShip collision;
    }
    void SpaceShip::hitSpaceStation(SpaceStation &otherObject)
    {
    SpaceStation &station = dynamic_cast<SpaceStation&>(spaceStation);
    // process a SpaceShip-SpaceStation collision;
    }
    void SpaceShip::hitAsteroid(Asteroid &otherObject)
    {
    Asteroid &theAsteroid = dynamic_cast<Asteroid&>(asteroid);
    // process a SpaceShip-Asteroid collision;
    }

    void SpaceShip::collide(GameObject &otherObject)
    {
    // 找出调用的函数
    HitFunctionPtr hfp = lookup(otherObject);
    if (hfp) (this->*hfp)(otherObject);
    else throw CollisionWithUnknownObject(otherObject);
    }

    SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
    {
    HitMap *phm = new HitMap;
    (*phm)["SpaceShip"] = &hitSapceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;
    return phm;
    }

    SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject &whatWeHit)
    {
    static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
    HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
    if (mapEntry == collisionMap.end())
    return 0;
    return (*mapEntry).second;
    }
    跟一开始说的RTTI解法一样,GameObject class只含有一个碰撞处理函数,这个函数执行两个必要的single-dispatches中的第一个。而其他互动函数不再使用同一个collide名称,放弃了重载。
    这里我们需要交付给一个中介函数lookup一个GameObject,它会返回一个指向”当和GameObject相撞时“必须调用的函数的指针(函数指针)。为了能够动态映射某个member function指针,一个简单的方法是产生一个关系型(associative)数组,只要获得class名字,导出member function指针(key-value?)。
将自行仿真的虚函数表格(Virtual Function Tables)初始化
  1. 对于collisionMap的初始化问题,只需要写一个private static member function,名为initializeCollisionMap,用来初始化,然后返回值作为初值就可以了。然而返回值Map按值传递意味着构造和析构成本,如果返回指针,又要苦恼map对象的delete时宜,那么用smart pointer吧(见上面完整实现)。
使用”非成员(Non-Member)函数“的碰撞处理函数
  1. 当有新的class加入时,继承体系的每个类都需要添加处理新型碰撞的代码.这是因为此前的策略都是将处理碰撞的任务交由碰撞的某一方来执行,仿真虚函数表策略也不例外——每个class内含一个仿真的虚函数表,内含的指针也都指向成员函数。将碰撞处理函数设为non-member,就可以使得class定义式不包含碰撞处理函数,当需要添加碰撞处理函数时也就不需要修改class定义。将碰撞处理函数移出class外,成为中立的第三者处理,则构筑processCollision函数:
    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
    48
    49
    50
    51
    52
    53
    #include "SpaceShip.h"
    #include "SpaceStation.h"
    #include "Asteroid.h"

    // 匿名namespace具有文件内部static的功效
    namespace {

    // 主要的碰撞处理函数
    void shipAsteroid(GameObject &spaceShip, GameObject &asteroid);
    void shipStation(GameObject &spaceShip, GameObject &spaceStation);
    void asteroidStation(GameObject &asteroid, GameObject &spaceStation);
    ...
    // 次要的碰撞处理函数,只是为了实现对称性
    // 对调参数位置,然后调用主要的碰撞处理函数
    void asteroidShip(GameObject &asteroid, GameObject &spaceShip)
    { shipAsteroid(spaceShip, asteroid); }
    void stationShip(GameObject &spaceStation, GameObject &spaceShip)
    { shipStation(spaceShip, spaceStation); }
    void stationAsteroid(GameObject &spaceStation, GameObject &asetroid)
    { asteroidStation(asteroid, spaceSation); }
    ...
    // types/functions
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
    typedef map< pair<string, string>, HitFunctionPtr > HitMap;
    // 以两个char*字面常量产生一个pair<string, string>对象
    pair<string, string> makeStringPair(const char *s1, const char *s2)
    { return pair<string, string>(s1, s2); }
    //
    HitMap* initializeCollisionMap()
    {
    HitMap *phm = new HitMap;
    (*phm)[makeStringPair("SpaceShip", "Asteroid")] = &shipAsteroid;
    (*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
    ...
    return phm;
    }
    // 必须修改,以便接纳pair<string, string>对象
    HitFunctionPtr lookup(const string class1, const string &class2)
    {
    static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
    HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
    if (mapEntry == collisionMap->end()) return 0;
    return (*mapEntry).second;
    }

    }

    void processCollision(GameObject &object1, GameObject &object2)
    {
    HitFunctinoPtr phf = lookup(typeid(object1).name(), typeid(object2).name());
    if (phf) phf(object1, object2);
    else throw UnknownCollision(object1, object2);
    }
    这份实现和先前的member functions版相同,但略有差异:
    • HitFunctionPtr如今是一个指向non-member function的指针。
    • exception class CollsionWithUnknownObject已经被重新命名为UnknownCollision并改为取得两个对象。
    • lookup需要接收两个类型名称,并执行double-dispatch的完整两半。
    这意味着collision map需要持有三份信息:两个类型名称和一个HitFunctionPtr
    由于makeStringPair,initializationCollisionMap,lookup都声明于匿名namespace内,因此它们必须实现于相同的namesapce中,使得链接器能够正确的将定义和声明关联起来
    通过将碰撞处理函数从类中分离,实现了即使新的GameObject被添加,原有的class也不需要重新编译,只需要在initializeCollisionMap中增加对应的键-值对,并在processCollision所在的匿名命名空间中申明一个新的碰撞处理函数即可
”继承“ + ”自行仿真的虚函数表格“
  1. 目前所做的每一件事都可以有效运作——只要在调用碰撞处理函数时不发生inheritance-based类型转换
    1
    2
    3
    4
    5
    6
    graph BT
    A[SpaceStation] --> G
    S(SpaceShip) --> G(GameObject)
    B[Asteroid] --> G
    C[CommercialShip] --> S
    D[MilitaryShip] --> S
    如果MilitaryShip和一个Asteroid碰撞,希望调用的时:
    1
    void shipAsteroid(GameObject &spaceShip, GameObject &asteroid);
    然而事非如此,而是抛出一个UnknownCollision exception。虽然MilitaryShip对象可视为一个SpaceShip对象,但lookup并不知道。
    如果想要实现double-dispatching而且需要支持inheritance-based参数转换,那么唯一可用的资源是”双虚函数调用“机制
将自行仿真的虚函数表格初始化(再度讨论)

条例16:谨记80-20原则

  1. 80-20原则说:一个程序80%的资源用于20%的代码身上。因此根据观察或实验来识别出造成你心痛那20%代码,辨别之道就是使用某个程序分析器(program profiler)。

条款17:考虑使用lazy evaluation(缓式评估)

  1. lazy evaluation缓式评估)就是以某种方式撰写你的classes,使它们延缓运算,直到那些运算结果刻不容缓地迫切需要为止。其运算结果一直不需要,运算也就一直不执行。lazy evaluation有以下4种常见用途:
    • Reference Counting(引用计数)。可避免非必要的对象赋值。lazy做法可以省下许多工作,string s1和string s2是两个字符串对象,我们让s2以s1为初值,而当s2的值需要修改时,才求取其值。那么我们可以在s2在需要被修改前,使用s1和s2的共享数据,直到需要被修改时才创建s2副本。
    • 区分读和写。对于一个类的operator[]调用动作,可能为了写或读,而运用lazy evaluatino和proxy classes,可以延缓决定究竟是读是写。
    • lazy fetching(缓式取出)。可避免非必要的数据库读取动作。产生一个对象时,只产生相应的“外壳”,不从磁盘读取任何数据,直至被需要时,才求取。这个可以使用smart pointer或者mutable修饰在const member functions里头判断并决定是否求取赋值。
    • lazy expression evaluation(表达式缓评估)。可避免非必要的数值计算动作。

条款18:分期摊还预期的计算成本

  1. 与lazy evaluation不同的是,超急评估over-eager evaluation)超前进度地做“要求以外”的更多工作。举个例子,有个class template用来表现大型数据收集中心:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class NumbercalType>
    class DataCollection
    {
    public:
    NumercalType min() const;
    NumercalType max() const;
    NumercalType avg() const;
    ...
    };
    这个函数的实现方法有三种:
    • eager evaluation:被调用时返回结果。
    • lazy evaluation:函数的返回值需要被派上用场时,决定适当值。
    • over-eager evaluation:随实记录程序执行过程中数据集的最小值、最大值和平均值。一旦被调用,立刻返回正确值无需再计算。这种方式通过频繁的调用摊还成本
    Caching时“分期摊还预期计算成本”的一种做法。Prefetching(预先取出)则是另一个种做法。可以想象它是大批购买物品后的折扣。因为磁盘控制器从磁盘读取数据时,读的是整个数据块或sectors。经验显示,如果某处数据被需要,通常其邻近的数据也被需要。这就是有名的locality of reference现象。系统设计者以此现象设计出磁盘缓存disk caches)、指令与数据的内存缓存memory caches)以及指令预先取出instruction prefetches)。
  2. 加入要实现一个动态数组的template:
    1
    2
    3
    4
    5
    template<class T>
    class DynArray { ... };
    DynArray<double> a;
    a[22] = 3.5;
    a[32] = 0;
    它怎么扩张自己?做内存动态分配行为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<clas T>
    T& DynArray<T>::operator[](int index)
    {
    if (index < 0)
    throw an exception;
    if (index > the current maximum index value) {
    int diff = index - the current maximum index value;

    call new to allocate enough additional memory so that index+diff is valid;
    }
    return the indexth element of the array
    }
    增加数组大小会调用new,但new会调用operator new,而operator new通常代价昂贵,因为它们会调用底层操作系统,而系统调用比进程内的函数调用速度慢,所以尽量不要系统调用。这时就可以使用over-eager evaluation(超急评估)策略,通过增加额外的数组大小来降低不久的下次可能再扩张时的成本。

条款19:了解临时对象的来源

  1. 程序员交谈的时候往往把一个短暂需要的变量称为临时变量,但在C++中它并不是临时对象,只是函数中的一个局部对象。C++真正的临时对象是不可见的。只要产生一个non-heap object而没有为它命名,就是临时对象。这种匿名对象发生于两种情况:一是隐式类型转换(implicit type conversions);二是函数返回对象
    • 第一种考虑为了让函数调用成功而产生的临时对象,发生在传递某对象给一个函数,而其类与它即将绑定上去的参数类型不同:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 返回ch在str中出现的个数
      size_t countChar(const string &str, char ch);
      char buffer[MAX_STRING_LEN];
      char c;
      // 读入一个char和一个string,利用setw避免读入string时缓冲区满溢的情况
      std::cin >> c >> setw(MAX_STRING_LEN) >> buffer;
      cout << "There are " << countChar(buffer, c)
      << " occurrences of the character " << c
      << " in " << buffer << endl;
      编译器擦除了类型不吻合的状态:以buffer作为自变量,调用string constructor,countChar的str参数绑定于此string临时对象上。当countChar返回,此临时对象自动销毁。只有当对象以by value或当对象传给一个reference-to-const参数时转换才会发生。
    • 第二种会产生临时对象的情况是当函数返回一个对象时。例如operator+的返回对象。

条款20:协助完成“返回值优化(RVO)“

  1. 函数如果返回对象,对于效率狂来说是个严重的溃败,因为以by-value方式返回对象,背后隐藏的constructor和destructor都将无法消除。而如果以by-pointer方式返回对象,对于其删除指针的操作通常会遗忘而导致资源泄露(resource leaks)。甚至以by-reference返回对象,那也是一个错误的行为,它返回的reference指向一个不再存活的对象。
  2. 我们可以用某种特殊写法来撰写函数, 使它在返回对象时能够让编译器消除临时变量的成本。这个伎俩就是:返回所谓的constructor arguments以取代对象:
    1
    2
    3
    4
    5
    // 返回对象:最有效率的做法。
    inline const Rational operator*(const Rational &lhs, const Rational &rhs)
    {
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
    它返回了一个表达式,该表达式调用了一个Rational constructor,这是一个临时对象,而函数复制此临时对象。
    C++允许编译器将临时对下个你优化。编译器消除operator*内的临时对象及被operaotr*返回的临时对象。它可以让return表达式所定义的对象构造于所绑定的命名对象的内存内。如果编译器这么做,调用operator*的临时对象的总成本为0。甚至还可以声明为inline,以消除调用operator*所花费的额外开销。这种特殊的优化行为甚至有个专属名称:return value optimization

条款21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)

  1. 对于解决可能操作符运算隐式的内置类型转换为用户定制类型产生的临时变量,有个办法是为该操作符重载一个内置参数的版本。

条款22:考虑以操作符符合形式(op==)取代其独身形式(op)

  1. 大部分程序员希望,如果他们能x = x + y;这么写,那么他们也能够x += y这么写。如果x和y属于定制类型,就不保证如此,到目前为止C++并不考虑在operator+,operator=和operator+=之间设立任何互动关系。除非自己实现。确保操作符的复合形式(operator+=)和独身形式(operator+)之间的自然关系能够存在,一个好办法就是以前者为基础实现后者
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<class T>
    const T operator+(const T &lhs, const T &rhs)
    { return T(lhs) += rhs; }

    Rational a, b, c, d, result;
    // 会用到3个临时变量
    result = a + b + c + d;
    // 不需要临时变量,效率比上面的好
    // 如果同时供应两种选择,便于客户较易理解的操作符独身版之余,同时保留了更有效率的符合版本
    result = a;
    result += b;
    result += c;
    result += d;
    T(lhs)是个调用动作,调用T的copy constructor,它会产生一个临时变量,然后临时变量被用来调用operator+=:
    1
    2
    3
    4
    5
    6
    template<class T>
    const T operator+(const T &lhs, const T &rhs)
    {
    T result(lhs); // 将lhs复制发给result
    return result += rhs; // 将rhs加到result上然后返回
    }
    这个template几乎等同于上一个,但是有个重要差异。第二个template内含了一个命名对象,这意味着返回值优化return value optimization)无法施展于此operator+。

条款23:考虑使用其他程序库

  1. 理想的程序库应该小、快速、威力强大、富弹性、有扩展性、直观、可广泛使用、有良好支持,使用时没有束缚,而且没有臭虫。然而这时不存在的。如果针对大小和速度做优化,通常不具移植性,如果有丰富性能,就不容易直观,没有臭虫的程序库只能在乌托邦中寻找。
  2. 检验一个极为简单的性能评估软件,它只测试基本的IO机能。这个程序从标准输入读取30,000个浮点数,然后以固定格式将它们写到标准输出设备。程序用iostream还是stdio取决于预处理器符号STDIO:
    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
    #ifdef STDIO
    #include <stdio.h>
    #else
    #include <iostream>
    #include <iomanip>
    #endif

    const int VALUES = 30000;

    int main(int argc, char **argv)
    {
    double d;
    for (int n = 1; n <= VALUES; ++n)
    {
    #ifdef STDIO
    scanf("%lf", &d);
    printf("%10.5f", d);
    #else
    std::cin >> d;
    std::cout << std::setw(10)
    << std::setprecision(5)
    << std::setiosflags(std::ios::showpoint)
    << std::setiosflags(std::ios::fixed)
    << d;
    #endif
    if (n % 5 == 0)
    {
    #ifndef STDIO
    printf("\n");
    #else
    std::cout << '\n';
    #endif
    }
    }
    return 0;
    }
    iostreams也可以产生固定格式的I/O,虽然不易写。但是operator<<不但类型安全而且可扩充,而printf两者皆否。stdio版有时候比iostream只是块一点点(20%),有时候很多(200%)。

条款24:了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

  1. 了解编译器以什么样的方法来实现它们是件重要的事情。其中最重要的就是虚函数。当一个虚函数被调用,执行的代码对应于调用者对象的类型。对象的pointer或reference,其类型是无形的,编译器是用virtual tablesvirtual table pointers,通常被简写为vtbls和vptrs。
    • vtbl通常是由函数指针架构而成的数组(有些是用链表)。程序中每个class凡声明(或继承)虚函数者,都有自己的一个vtbl,而其中的条目entires)就是该class的各个虚函数实现体的指针。这就是虚函数的第一个成本必须为每个拥有虚函数的class耗费一个vtbl空间,空间大小视虚函数的个数(包括继承而来的)而定
    • 凡声明由虚函数的class,其对象都含有一个隐藏的data member,用来指向该class的vtbl。这个隐藏的data member——所谓的vptr——被编译器加入对象内某个只有编译器才知道的位置。这是虚函数的第二个成本必须在每个拥有虚函数的对象内付出一个额外指针的代价
      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
      class C1
      {
      public:
      C1();
      virtual ~C1();
      virtual void f1();
      virtual int f2(char c) const;
      virtual void f3(const string &s);
      void f4() const;
      ...
      };

      class C2 : public C1
      {
      public:
      C2();
      virtual ~C2();
      virtual void f1();
      virtual void f5(char *str);
      ...
      };

      void makeACall(C1 *pC1)
      {
      pC1->f1();
      }
      编译器必须产生代码,完成以下动作:
    1. 根据对象的vptr找出其vbl。因为编译器知道去哪里找vptr。成本只有一个偏移调整offset adjustment,以便获得vptr)一个指针间接动作(以便获得vtbl)
    2. 找出被调用函数(f1)在vtbl内的对应指针。成本只是一个差移(offset)
    3. 调用步骤2所得指针所指向的函数
      1
      2
      3
      4
      5
      6
      7
      8
      // 想象一下每个对象都有隐藏的data member称为vtpr,而函数发
      的vtbl索引是i
      // 那么先前语句
      pC1->f1();
      // 产生的代码将是:
      (*pC1->vptr[i])(pC1);
      // 调用pC1->vptr所指的vtbl中的第i条目所指函数。
      // 然后pC1被传给该函数的this指针所用
      这几乎和一个非虚函数的效率相当。它调用虚函数的成本基本和通过一个函数指针来调用函数相同。虚函数本身不构成性能上的瓶颈。
      虚函数真正的运行期成本发生在和inlining互动的时候。虚函数不应该inlined。因为inline意味着在编译期,将调用端的调用动作被调用函数的函数本体所取代,而virtual则意味着等待,直到运行期间才知道哪个函数被调用。这就是虚函数的第三个成本事实上等同于放弃了inlining。(如果虚函数通过对象被调用,是可以inlined的,但大部分虚函数调用动作是通过对象的指针或references完成的,这行为无法被inlined,所以虚函数等于无法被inlined。)
      在多重继承的情况下,情况变得有些复杂,一个对象可能包含多个vptr(每个base class各对应一个),这使得空间负担更大,运行期的调用成本也有增长。
      多重继承往往导致virtual base classes(虚拟基类)的需求。virtual base classes可导致另一种成本,当利用指针或references指向某个virtual base class成分时,对象内可能出现多个这样的指针:
      1
      2
      3
      4
      class A { ... };
      class B : virtual public A { ... };
      class C : virtual public A { ... };
      class D : public B, public C { ... };
  2. 运行时期类型辨识runtime type identification, RTTI)让我们在运行期获得objects和classes的相关信息,它们被存放在类型为type_info的对象内。可以利用typeid操作符取得某个class相应的type_info对象。只有当某种类型拥有至少一个虚函数,才保证我们能检验该类型对象的动态类型。它是个根据class的vtbl来实现的。RTTI的空间成本就只需在每个class vtbl内增加一个条目,再加上每个class所需的一份type_info对象空间。
    性质 对象大小增加 Class数据量增加 Inlining几率降低
    虚函数
    Virtual Functions
    多重继承
    Multiple Inheritance
    虚拟基类
    Virtual Base Classes
    往往如此 有时候
    运行时期类型标识
    RTTI

C++增加了exceptions性质后,可能令人不舒服的改变了很多事情。原始指针的使用如今称为高风险行为,资源泄露的机会大增,撰写符合期望的constructors和destructors的难度也大增。程序员特别小心,放置程序在执行时突然中止。可执行文件和程序库变得更大、速度更慢。
为什么使用exceptions?答案很简单:exceptions无法被忽视。如果一个函数利用设定状态变量的方式或是利用返回错误码的方式发出一个异常,无法保证此函数的调用者会检查那个变量或检验那个错误码。于是程序不断执行,远离错误发生地点。但如果函数以抛出exception的方式发出异常信号,而该exception未被捕捉,程序的执行立刻中止。
C只有setjmplongjmp才能近似这样的行为。但longjmp在C++中有缺陷:当它调整栈(stack)的时候,无法调用局部(local)对象的destructors。C++很依赖destructors被调用。

条款9:利用destuctors避免资源泄露

  1. 有个例子,加入你现在要写一个软件。收养中心每天会产生一个文件,其中有它所安排的当天收养个案,你的工作是写程序读文件,然后为每个个案做适当处理:
    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
    // ALA: abstract base class
    class ALA
    {
    public:
    virtual void processAdoption() = 0;
    ...
    };
    class Puppy : public ALA
    {
    public:
    virtual void processAdoption();
    ...
    };
    class Kitten : public ALA
    {
    public:
    virtual void processAdoption();
    ...
    };
    ALA* readALA(istream &s);
    // 程序核心大致
    void processAdoptions(istream &dataSource)
    {
    // 如果还有数据
    while (dataSource)
    {
    // 取出下一只动物
    ALA *pa = readALA(dataSource);
    // 处理收养事宜
    pa->processAdoption();
    // 删除readALA返回的对象
    delete pa;
    }
    }
    如果pa->processAdoption抛出一个exception怎么办,很简单:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void processAdoptions(istream &dataSource)
    {
    while (dataSource)
    {
    ALA *pa = readALA(dataSource);
    try {
    pa->processAdoption();
    } catch (...) { // 捕捉
    delete pa; // delete,避免泄露
    throw; // 传播给调用端
    }
    delete pa;
    }
    }
    这样程序被try语句块和catch语句块搞得乱七八糟。解决方法是以一个类似指针的对象取代指针pa,这样当这个类指针对象被(自动)销毁,可以令其destructor调用delete。这就是smart pointers。C++标准库有,它看起来是这样子的(此书有点旧):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class T>
    class auto_ptr
    {
    public:
    auto_ptr(T *p = 0) : ptr(p) {}
    ~auto_ptr() { delete ptr; }
    private:
    T *ptr;
    }
    auto_ptr不适合delete数组对象的指针。如果你希望有这样的auto_ptr用在数组上,要么自己写一个,要么最好使用一些容器,比如vector来取代。用auto_ptr对象取代原始指针后,processAdoptions看起来这样子:
    1
    2
    3
    4
    5
    6
    7
    8
    void processAdoptions(istream &dataSource)
    {
    while (dataSource)
    {
    auto_ptr<ALA> pa(readALA(dataSource));
    pa->processAdoption();
    }
    }
    它和原版本的差异只有两处。一是pa被声明为一个auto_ptr<ALA>对象;二是循环最后不再有delete语句。它是隐藏在auto_ptr背后的观念——以一个对象存放必须自动释放的资源,并依赖该对象的destructor释放。

条款10:在constructors内阻止资源泄露(resource leak)

  1. 对于尚未完全构造好的对象,C++拒绝调用其destructor。由于C++不自动清理构造期间抛出exceptions的对象,所以设法让constructor在那种情况下也能自我清理。这通常将可能的exceptions捕捉起来,执行清理工作,然后重新抛出exception。让它继续传播:
    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    class Image 
    {
    public:
    Image(const string &imageDataFileName);
    ...
    };

    class AudioClip
    {
    public:
    AudioClip(const string &audioDataFileName);
    ...
    };

    class BookEntry
    {
    public:
    ...
    ~BookEntry();
    BookEntry(const string&, const string&, const string&, const string&);
    void addPhoneNumber(const PhoneNumber &number);
    ...
    private:
    void cleanup();
    string theName;
    string theAddress;
    list<PhoneNumber> thePhones;
    Iimage * const theImage;
    AudioClip * const theAudioClip;
    Image* initImage(const string &imageFileName);
    AudioClip* initAudioClip(const string &audioClipFileName);
    };

    void BookEntry::cleanup()
    {
    delete theImage;
    delete the AudioClip;
    }

    /*
    BookEntry::BookEntry(const string &name,
    const string &address,
    const string &imageFileName,
    const string &audioClipFileName)
    : theName(name, theAddress(address),
    theImage(0), theAudioClip(0)
    {
    try {
    if (imageFileName != "")
    theImage = new Image(imageFileName);
    if (audioClipFileName != "")
    theAudioClip = new AudioClip(audioClipFileName);
    } catch (...) {
    cleanup();
    throw;
    }
    }
    */

    // 想要在exceptions传播到constructor外之前,无法将try和catch放到member initialization list中,就放到某些private member functions中。
    BookEntry::BookEntry(const string &name,
    const string &address,
    const string &imageFileName,
    const string &audioClipFileName)
    : theName(name, theAddress(address),
    theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName))
    {}

    Image* BookEntry::initImage(const string &imageFileName)
    {
    if (imageFileName != "")
    return new Image(imageFileName);
    else return 0;
    }

    AudioClip* BookEntry::initAudioClip(const string &audioClipFileName)
    {
    try {
    if (audioClipFileName != "")
    return new AudioClip(audioClipFIleName);
    else return 0;
    } catch (...) {
    delete theImage;
    throw;
    }
    }

    BookEntry::~BookEntry()
    {
    cleanup();
    }
    更好的做法是如条款9一样,使用smart pointer来取代pointer class members,免除了exception出现时发生资源泄露。

条款11:禁止异常(exceptions)流出destructors之外

  1. 两种情况下destructor会被调用。第一种情况是当对象在正常状态下被销毁,也就是离开生存空间(scope)或是被明确删除时;第二种情况是当对象被exception处理机制——也就是exception传播过程中的stack-unwinding(栈展开)机制——销毁
  2. 当destructor被调用,可能有一个exception正在作用中,可惜的是无法在destructor区分这些状态(有了)。这样就要保守撰写destructors了。如果控制权基于exception的因素离开destructor,而此时另一个exception处于作用状态,C++会调用terminate函数,将程序结束掉。
  3. 有两个理由支持阻止exceptions传出destructor之外。第一,它可以避免terminate函数在exception传播过程的栈展开stack-unwinding)机制被调用;第二,它可以协助确保destructors完成其应该完成的所有事情

条款12:了解”抛出一个exception“与”传递一个参数“或”调用一个虚函数“之间的差异

  1. 从抛出端传递一个exception到catch字句,看起来和从函数调用端传递一个自变量到函数参数是一样的。但是有重大的不同。函数参数和exception的传递参数有3中:by value,by reference,by pointer。然而,当你调用一个函数,控制权最终会回到调用端(除非函数失败),但是当你抛出一个exception,控制权不再回到抛出端。而且被捕捉的exception是某个对象的副本,即使以by reference方式传参。
  2. 当对象被赋值当作一个exception,复制行为是由对象的copy constructor执行的。这个copy constructor相当于该对象的”静态类型“而非”动态类型”。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Widget { ... };
    class SpecialWidget : public Widget { ... };
    void passAndThrowWidget()
    {
    SpecialWidget localSpecialWidget;
    ...
    // rw表示一个SpecialWidget
    Widget& rw = localSpecialWidget;
    // 抛出一个类型为Widget的exception
    throw rw; // (条款25有返回动态类型的技术)
    // 抛出一个类型为SpecialWidget的exception;这是因为它并没有发生复制行为,只是再抛出。
    throw;
    }
    catch (Widget w) ... // by value
    catch (Widget &w) ... // by reference
    catch (const Widget &w) ... // by reference-to-const
    以上“参数传递”和“exception传播“之间的另一个区别。exception的复制动作,其结果是个临时对象。一个被抛出的对象可以简单的用by reference的方式捕捉,不需要以by reference-to-const的方式捕捉。调用过程将一个临时对象传递给non-const reference参数是不允许的,但对exceptions则是合法。
    值得一提,by value方式传递exception,得符出”被抛出物“两个副本的构造代价,其中一个构造动作用于产生exception临时对象,另一个构造动作作用于将临时对象赋值到w;而by reference得付出单一副本的构造代价,这个副本就是临时副本,by reference方式传递函数参数不会发生复制行为。
    还有,exceptions与catch子句匹配过程中,不会发生隐式转换的行为。它仅有两种转换可以发生。第一种是继承架构中的类转换inheritance-based conversions)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    graph BT
    logic_error-->exception
    runtime_error-->exception
    domain_error-->logic_error
    invalid_argument-->logic_error
    length_error-->logic_error
    out_of_range-->logic_error
    range_error-->runtime_error
    underflow_error-->runtime_error
    overflow_error-->runtime_error
    1
    2
    3
    4
    5
    6
    catch (runtime_error) ...
    catch (runtime_error&) ...
    catch (const runtime_error&) ...
    catch (runtime_error*) ...
    catch (const runtime_error*) ...
    catch (const void*) ...
    ”传递参数“和”传播exception“的最后不同是,catch子句总是依出现顺序做匹配尝试。将此行为和调用虚函数的行为比对。当调用虚函数时,被调用的函数是“调用者的动态类型”中的函数。在这方面,虚函数采用”best fit“(最佳吻合)策略exception遵循”first fit”(最先吻合)策略。所以针对derived class而设计的catch子句必须放到针对base class设计的catch子句之前。

条款13:以by reference方式捕捉exceptions

  1. throw by pointer应该是最有效率的一种做法,因为它不复制对象。但它并没有想象中的好,因为了让exception objects在控制权离开抛出指针的函数之后依然存在,就需要声明global或static对象,而程序很容易忘记。而且如果抛出的指针指向一个新的heap-based,负责catch的人不能区分是global即static还是heap中的exception object,于是可能产生内存泄露或者未定义的行为等潜在问题。
    此外,catch-by-pointer和语言本身建立起来的惯例有矛盾。4个标准的exception——bad-alloc、bad_cast、bad_typeid和bad_exception——统统都是对象,不是对象指针。说明只能使用by value或by reference。然而catch-by-value每当exception objects被抛出,就得复制两次。而且他会引起切割slicing)问题,因为derived class exception objects被捕捉并被视为base class exceptions:被切割过的对象其实就是缺少derived class data members。
    所以接下来就是catch-by-reference了。它不像catch-by-pointer发生对象删除问题,也不存在catch-by-value的切割问题,而且exception objects只会被复制一次。

条款14:明智运用exception specifications

  1. exception specifications让代码更容易被理解,因为它明确指出一个函数可以抛出什么样的exceptions。但它不只是一个注释而已。编译器有时候能够在编译期间侦测到与exception specifications不一致的行为。如果函数抛出了一个并未列入于其exception speicification的exception,这个错误会在运行期被检验出来。于是特殊函数unexpected会被自动调用。unexpected的默认行为是调用terminate,而terminate的默认行为是调用abort,所以程序如果违反exception speicification,默认结果就是程序被中止。
  2. 编译器只会对exception specifications做局部经检验,调用某个函数而该函数可能违反调用端函数本身的exception specification。这么做可能会导致程序被迫终止,为将这种不一致性降到最低,一个方法是避免将exception specification放在需要类型自变量的templates身上
    1
    2
    3
    4
    5
    6
    // 不良的temmplates design,它带有exception specification
    template<class T>
    bool operator==(const T &lhs, const T &rhs) throw()
    {
    return &lhs == &rhs;
    }
    它指明template产生出来的函数不会抛出任何exception,然而它调用的operator &可能会抛出一个异常。更一般化的问题是,没有任何方法可以知道一个template的类型参数可能抛出什么。
    避免踏上unexpected之路的第二个方法是:如果A函数调用的B函数没有exception specifications,那么A函数本身也不要设定exception specifications
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 标准委员会宣称typedef内不可出现exception specification, 可能需要该写宏
    typedef void (*CallBackPtr)(int eventXLocation, int eventYLocation, void *dataToPassBack) throw();

    class CallBack
    {
    public:
    CallBack(CallBackPtr fPtr, void *dataToPassBack) : func(fPtr), data(dataToPassBack) {}
    vvoid makeCallBack(int eventXLocation, int eentYLocaltion) const throw();s
    private
    CallBackPtr *func;
    void *fdata;
    }

    void CallBack::makeCallBack(int eventXLocation, int eventYLocatino) const throw()
    {
    fnuc(eventXLocation, eventYLocation, data);
    }
    第三个技术是:处理“系统”可能抛出的exceptions。直接处理非预期的exceptions比事先预防来的简单。但如果你写的软件大量运用exception specifications,但调用程序库提供的函数没有使用exception specifications,想要阻止之变得不切实际。这时候可以利用一个事实:以不同类型的exceptions取代非预期的exceptions。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 所有非预期的exception objects都被取代为此类的objects
    class UnexpectedException {};
    // 如果有非预期的exception被抛出,调用此函数
    void convertUnexpected()
    {
    throw UnexpectedException();
    }
    // 以convertUnexpected取代默认的unexpected
    std::set_unexpected(convertUnexpected);
    一旦完成这些部署,非预期的exception会导致该函数被调用。那么只要违反的exception specification内含有Unexpected Exception,exception的传播就会继续下去,而如果未含有,terminate会被调用,犹如未取代unexpected一样。
    将非预期的exceptions转换为一个已知类型的另一个做法是,依赖以下事实:如果非预期函数的替代者重新抛出当前(current)exception,该xception会被保准类型bad_exception取代而之。

条款15:了解异常处理(exception handing)的成本

  1. 为了能够在运行期处理exceptions,程序必须做大量簿记工作。在每个执行点,它们必须能够确认“如果发生exception,哪些对象需要析构”,它们必须在每个try语句块的进入点和离开点做记号,针对每个try语句块它们必须记录对应的catch子句及能够处理的exceptions类型。这些簿记工作必须符出代价,运行时期的比对工作(以确保符合exception specifications)不是免费的。

条款5:对定制的“类型转换函数”保持警觉

  1. C++允许编译器在不同类型之间执行隐式转换implicit conversions)。它继承C的传统,允许将char转换为int,将short转换为double。然而还有令人害怕的转型,包括将int转换为short,将double转换为char,这些类型转换会遗失信息。所以我们可以使用自己的类型。两种函数允许编译器执行这样的转换:单自变量constructors隐式类型转换操作符。所谓的单自变量constructor是指能够以单一自变量成功调用的constructors:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Name
    {
    public:
    // string to Name
    Name(const std::string &s);
    ...
    };
    class Rational
    {
    public:
    ...
    // Rational to double
    operator double() const;
    }
    ,而隐式类型转换操作符,是一个拥有奇怪名称的member function:关键词operator后面加上一个类型名称,你不能为此函数指定返回值类型,因为其返回值类型已经表现在函数名称上了:
    1
    2
    3
    4
    5
    6
    7
    8
    class Rational 
    {
    public:
    ...
    operator double() const;
    };
    Rational r(1, 2); // r == 1/2
    double d = 0.5 * r; // 将r转为double,然后执行乘法运算
  2. 假设有一个class用来表现分数(rational numbers)。你希望像内建类型一样地输出Rational objects内容。
    1
    2
    Rational r(1, 2);
    std::cout << r;
    假设忘了给Rational写一个operator<<,上述打印不但不会出错,编译器在面对上述动作,它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功。而之间的代码发现,只要调用了Rational::oeprator double,将r转换为double,调用动作就能成功,却又有隐式类型转换操作符的缺点:它的出现可能到导致错误(非预期)的函数被调用。于是有了解决办法:
    1
    2
    3
    4
    5
    6
    7
    8
    class Rational
    {
    public:
    ...
    double asDouble() const;
    };
    std::cout << r; // error
    std::cout << r.asDouble(); // ok, 以double形式输出r
    必须明白调用类型转换函数虽然有点不便,却可以避免默默调用了不想调用的函数。这也就是为什么标准程序库的string类型并未含有从string object到C-style char*的隐式转换函数,而是提供了一个显式的member function c_str
    在单自变量constructors,这些函数造成的隐式类型转换的情况更难对付:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template<class T>
    class Array
    {
    public:
    Array(int lowBound, int highBound);
    Array(int size);
    T& operator[](int index);
    ...
    };
    bool operator==(const Array<int> &lhs, const Array<int> &rhs);
    Array<int> a(10);
    Array<int> b(10);
    ...
    for (int i = 0; i < 10; ++i)
    if (a == b[i]) // 如果打算写a[i] == b[i]
    {
    do something for when a[i] and b[i] are equal;
    }
    else
    do something for when they're not;
    这样的错误编译器并没有提醒,它一声不吭,编译器注意到它,发现只要调用Array<int> constructor(需要一个int自变量)就可以将int转为Array<int> object。于是它放手去做。
    有两种办法阻止编译器这么做:一个是简易法,另一个可在编译器不支持简易法的情况下使用。
    简易法是使用C++特征:关键词explicit。只要将constructors声明为explicit,编译器就能因隐式转换而调用它,不过显式类型转换是允许的(没发现)。有个解决方案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template<class T>
    class Array
    {
    public:
    class ArraySize
    {
    public:
    ArraySize(int numElements) : theSize(numElements) {}
    int size() const { return theSize; }
    private:
    int theSize;
    };
    Array(int lowBound, int highBound);
    Array(ArraySize size);
    ...
    };
    bool operator==(const Array<int> &lhs, const Array<int> &rhs);
    Array<int> a(10);
    Array<int> b(10);
    ...
    for(int i = 0; i < 10; ++i)
    if (a == b[i]) // error
    编译器需要一个Array<int>的对象在==右边,但此时没有这样隐式转换的constructor。更不能将int转换一个临时性的ArraySize对象,再根据这个临时对象产生必要的Array<int>对象,所以报错。类似ArraySize这样的classes,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人proxy)一样。这是一个很值得学习的技术。

条款6:区别increment/decrement操作符的前置(prefix)和后置(postfix)形式

  1. 重载函数是以其参数类型来区分彼此的,然而不论increment或decrement操作符的前置式或后置式,都没有参数,为了填平这个语言学上的漏洞,只好让后置式有一个int自变量,并且让它被调用时,编译器默默地为该int指定一个0值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class UPInt
    {
    public:
    UPInt& operator++(); // 前置式++
    const UPInt operator++(int); // 后置式++
    UPInt& operator--(); // 前置式--
    const UPInt operator--(int); // 后置式--
    UPInt& operator+=(int); // +=操作符
    ...
    };
    UPInt i;
    ++i; // i.operator++();
    i++; // i.operator++(0);
    --i; // i.operator--();
    i--; // i.operator--(0);
  2. 所谓的increment操作符的前置式意义“increment and fetch“(积累然后取出),后置式意义”fetch and increment“(取出然后累加):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    UPInt& UPInt::operator++()
    {
    *this += 1;
    return *this;
    }
    const UPInt UPInt::operator++(int)
    {
    UPInt oldValue = *this;
    ++(*this);
    return oldValue;
    }
    为什么后置increment操作符返回的对象是const呢?以下:
    1
    2
    3
    UPInt i;
    i++++; // 后置increment操作符两次
    i.operator(0).operator++(0);

条款7:千万不要重载&&,||和,操作符

  1. 操作符重载也不是任何都能用来重,它也是有底线的,你不能重载以下操作符:
    1
    2
    3
    .       .*      ::      ?:      new 
    delete sizeof typeid static_cast
    dynamic_cast const_cast reinterpret_cast
    可以重载的有:
    1
    2
    3
    4
    5
    6
    7
    operator new        operator delete
    operator new[] operator delete[]
    + - * / % ^ & | ~
    ! = < > += -= *= /= %=
    ^= &= |= << >> >>= <<= == !=
    <= >= && || ++ -- , ->* ->
    () []

条款8:了解各种不同意义的new和delete

  1. new operator和operator new之间是有差别的。

    1
    string *ps = new string("Memory Management");

    所使用的new是所谓的new operator。这个操作符是语言内建的,跟sizeof一样不能改变意义。它的分为两方面:分配足够的内存用来存放某类型的对象而后调用一个constructor为刚才分配的内存中的那个对象设定初值
    我们能够重写改函数,改变其行为。这个函数名称叫operator new。它的函数原型是:

    1
    void * operator new(size_t size);

    size_t参数表示需要分配多少内存。然后返回一块原始的内存。和malloc一样,operator new唯一任务是分配内存。但它不知道什么是constructors。当你想要调用一个constructor,有个特殊版本的operator new,称为placement new,允许你这么做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Widget
    {
    public:
    Widget(int widgetSize);
    ...
    };
    Widget* constructorWidgetInBuffer(void *buffer, int widgetSize)
    {
    return new(buffer) Widget(widgetSize);
    }

    这是new operator的用法之一,指定一个额外自变量(buffer)作为new operator隐式调用operator new时所用。于是,被调用的operator new除了几首size_t变量外,还接受了一个void*参数,这就是所谓的placement new,看起来像这样:

    1
    2
    3
    4
    void* operator new(size_t, void *location)
    {
    return location;
    }

    如果你希望将对象产生于heap,请使用new operator,它不但分配内存而且为对象调用一个constructor。如果只是打算分配内存,调用operator new,那没有任何constructor被调用。如果打算在heap objects产生时决定内存分配方式,写一个自己的operator new。

  2. 为了避免resource leaks资源泄露),每一个动态分配行为都必须匹配一个相应但相反的释放动作。内存释放动作是由operator delete执行:

    1
    2
    3
    4
    string *ps;
    ...
    delete ps;
    void operator delete(void *memoryToBeDeallocated);

    因此,delete动作会产生类似代码:

    1
    2
    ps->~string();
    operator delete(ps);

    这里呈现的暗示是,如果只打算处理原始的、未设初值的内存,应该回避new operator和delete operators。如果使用了placement new产生对象,避免对该内存使用delete operator。因为delete operator会调用operator delete来释放内存,但该内存并非由operator new分配得来的。placement new只是返回它接受的指针而已,所以未了抵消该对象的constructor的影响,应该直接调用该对象的destructor。

  3. 面对数组情况,也有所不同:

    1
    string *ps = new string[10];

    上述的new仍然是new operator,但由于诞生的是数组,所以new operator的行为不再以operator new分配,而是由一个名为operator new[]负责分配(通常称为array new)。同样道理,当delete operator被用于数组,它会针对数组中的每个元素调用其destructor,然后调用operator delete[]释放内存。
    总结以下,new operator和delete operator都是内建操作符,无法为你所控制,但是它们所调用的内存分配/释放函数则不然。

条款1:指针与引用的区别

  1. 当你知道你必须指向一个对象并且不想改变其指向时,或者在重载符并为防止不必要的语义误解时,你不应该使用指针。除此之外的情况下,则使用指针。

条款2:尽量使用C++风格的类型转换

  1. 将一个pointer-to-const-object转型为一个pointer-to-non-const-object(改变对象常量性,和将一个pointer-to-base-class-object转型为一个pointer-to-derived-class-object(改变对象类型)有很大差异。而传统C转型动作并无区分,不过旧式的C转型动作并非唯一选择,C式转型式为C设计的,不是为了C++。C++导入4个新的转型操作符cast operators):static_castconst_castdynamic_castreinterpret_cast
  2. static_cast具有与C旧式转型相同的威力与意义,以及相同的限制。static_cast不能移除表达式的常量性(constness),因为有const_cast。
  3. 其他新式C++转型操作符适用于更集种(范围更狭窄)的目的。const_cast用来改变表达式中的常量性(constness)或变易性(volatileness)。常见的用途在于将某个对象的常量性去掉。
  4. dynamic_cast用来执行继承体系中“安全的向下转型或跨转型动作”。它不能应用在缺乏虚函数的类型身上。
  5. reinterpret_cast这个于编译平台有关,基本不具移植性。它的常用用途是转换“函数指针”类型:
    1
    2
    3
    4
    5
    typedef void (*FuncPtr)();
    FuncPtr funcPtrArray[10];
    int doSomething();
    funcPtrArray[0] = &doSomething; // error
    funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //ok

条款3:绝对不要以多态(polymorphically)方式处理数组

  1. 继承inheritance)的最重要性质之一是可以通过指向base class objects的pinters或references来操作derived class objects。我们说它行为是多态的polymorphically)。它也允许以上形成的数组。但这不值得沾沾自喜,因为它几乎不会如预期般运作。
    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
    class BST { ... };
    class BalancedBST : public BST { ... };
    void printBSTArray(std::ostream &s, const BST array[], int numElements)
    {
    for (int i = 0; i < numElements; ++i)
    s << array[i]; // 假设BST objects有个operator<<可用
    }

    BST BSTArray[10];
    printBSTArray(std::cout, BSTArray, 10); // ok
    // 编译器可能会被误导,它假设数组元素大小是BST的大小,通常derived classes比base classes大。
    // 那么会发生不可预期的事情??
    BalancedBST bBSTArray[10];
    printBSTArray(std::cout, bBSTArray, 10);

    void deleteArray(std::ostream &logStream, BST array[])
    {
    logStream << "Deleting array at address "
    << stati_cast<void*>(array) << '\n';
    delete []array;
    }
    BalancedBST &balTreeArray = new BalancedBST[50];
    deleteArray(std::cout, balTreeArray);
    // delete []array会产生这样的代码
    /*
    for (int i = the number of elements in the array -1; i >= 0; --i)
    array[i].BST::~BST(); // 调用array[i]的destructor
    */
    在C++语言规范中说,通过base class指针删除一个由derived classes objects构成的数组,其结果未定义

条款4:非必要不提供default constructor

  1. 所谓default constructor是C++一种无中生有的方式。constructors用来将对象初始化,所以default constructors的意思是在没有任何外来信息的情况将对象初始化。然而有些对象如果没有外来信息,是无法初始化的,可能产生的对象没有意义。

  2. 如果class缺乏一个default constructor,使用class时会有限制,第一个问题在产生数组的时候。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    class EquipmentPiece
    {
    public:
    EquipmentPiece(int IDNumber);
    ...
    };
    EquipmentPiece bestPieces[10]; // error
    EquipmentPiece *bestPieces = new EquipmentPiece[10]; // error

    在产生数组的时候,一般而言没有任何方法可以为数组中的对象指定constructor自变量。有三个方法可以解决这个束缚:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 第一个方式是使用non-heap数组
    // 不幸的是此法无法延伸至heap数组
    int ID1, ID2, ID3, ..., ID10;
    ...
    EquipmentPiece bestPieces[] =
    {
    EquipmentPiece(ID1),
    EquipmentPiece(ID2),
    EquipmentPiece(ID3),
    ...,
    EquipmentPiece(ID10)
    };

    // 更一般化的做法是使用“指针数组”而非“对象数组
    // 此法有两个缺点。
    // 第一,必须记得此数组所指的所有对象删除,否则会出现resource leak(资源泄露)
    // 第二,需要足够大的内存总量,需要一些空间来放置指针和一些空间来放置EqupmentPiece objects
    typedef EquipmentPiece *PEP;
    PEP bestPieces[10]; // ok,不需要调用ctor
    PEP *bestPieces = new PEP[10]; // ok
    for (int i = 0; i < 10; ++i)
    bestPieces[i] = new EquipmentPiece(ID number);

    过度使用内存这个问题可以避免:

    1
    2
    3
    4
    5
    6
    7
    // 分配足够的raw memory
    void *rawMemory = operator new[](10 * sizeof(EquipmentPiece));
    // 让bestPieces指向此块内存,将这块内存视为一个EquipmentPiece数组
    EquipmentPiece *bestPiece = static_cast<EquipmentPiece*>(rawwMemory);
    // 利用“placement new”构造这块内存中的objects
    for (int i = 0; i < 10; ++i)
    new(&bestPieces[i]) EquipmentPiece(ID number);

    placement new的缺点是,维护起来比较困难。此外,在数组内的对象结束生命时,以手动方式调用其destructors,最后还得调用operator delete[]的方式释放raw memory:

    1
    2
    3
    4
    5
    // 相反顺序析构掉
    for (int i = 0; i >= 0; --i)
    bestPieces[i].~EquipmentPiece();
    // free raw memroy
    operator delete[](rwaMemory);
  3. classes缺乏default constructors带来的第二个缺点是:它们不适用于许多template-based container classes。对template而言,被实例化instantiated)的目标类型必须有一个deault constructors。这是一个普遍的共同需求,那些templates内几乎总是会产生一个以“template类型参数”作为类型而架构起来的数组,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<class T>
    class Array
    {
    public:
    Array(int size);
    ...
    private:
    T *data;
    };
    template<class T>
    Array<T>::Array(int size)
    {
    // 数组中的每个元素都调用T::T()
    data = new T[size];
    ...
    }

    如果谨慎设计,可以消除对default constructor的需求。标准库中vector template(产生可扩展数组的各种classes)就不要求default constructor。

  4. 最后一个考虑点是virtual base classes。因为virtual base class constructors的自变量由最深层次的派生类的class提供。于是,一个缺乏default constructor的virtual base class,要求所有derived classes都必须知道、了解其意义,并且提供自变量。

这里有3个条款。第一个条款强调不可轻忽编译器警告信息。第二个条件总览C++
Standard,主要是TR1
。最后条款总览Boost。很多机能都被在C++之后新特性中不断引入把。

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

  1. 严肃对待编译器发出的警告信息。
  2. 不要过度依赖编译器的报警能力,不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,原本依赖的警告信息有可能消失。

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

  1. C++98列入C++标准程序库有以下成分:
    • STLStandard Template Library标准模板库)。覆盖容器(containers如vector,string,map)、迭代器(iterators)、算法(algorithms如find,sort,transform)、函数对象(function objects如less,greater)、各种容器适配器(container adapters如stack,priority_queue)和函数对象适配器(function object adapters如mem_fun,not1).
    • Iostreams,覆盖用户自定缓冲功能、国际化I/O,以及预先定义好的对象cin,cout,cerr和clog。
    • 国际化支持,多区域(multiple active locales)能力。像wchar_t(通常是16 bits/char)和wstring(由wchar_ts组成的stirngs)等类型促进Unicode。
    • 数值处理,包括复数模板(complex)和纯数值数组(valaray)。
    • 异常阶层体系(exception hierarchy),包括base class exception及其derived classes logic_error和runtime_error,以及更深继承的各个classes。
    • C89标准程序库。
  2. C++11相对于C++98/03有哪些显著的增强呢?
    • 通过内存模型、线程、原子操作等支持本地并行编程(Native Concurrency)
    • 通过统一初始化表达式、auto、decltype、移动语义等来统一对泛型编程的支持
    • 通过constexpr、POD等来更好地支持系统编程
    • 通过内联命名空间、继承构造函数和右值引用等来更好地支持库的构建
  3. C++11 知识点

    auto关键字、for-each循环、右值及移动构造函数 + std::forward + std::move + stl容器新增的emplace_back()方法、std::thread库、std::chrono库、智能指针系列(std::shared_ptr/std::unique_ptr/std::weak_ptr)(智能指针的实现原理一定要知道,最好是自己实现过)、线程库std::thread+线程同步技术库std::mutex/std::condition_variable/std::lock_guard等、lamda表达式(JAVA中现在也常常考察lamda表达式的作用)、std::bind/std::function库、其他的就是一些关键字的用法(override、final、delete),还有就是一些细节如可以像JAVA一样在类成员变量定义处给出初始化值。

条款55:让自己熟悉Boost

  1. Boost是C++开发者集结的社群。它和C++标准委员会之间有独一无二的密切关系,Boost由委员会成员创设,因此Boost成员和委员会成员由很大的重叠。Boost的目标是作为一个可被加入标准C++之各种功能的测试场。
  2. Boost程序库对付的主题非常繁多,包括:
    • 字符串与文本处理,覆盖具备类型安全(type-safe)的pointer-like格式化动作、正则表达式,以及语汇单元切割(tokenizing)和解析(parsing)。
    • 容器,覆盖数组、大小可变的bitsets以及多维数组。
    • 函数对象和高级编程,覆盖若干用来作为TR1机能基础的程序库。其中一个程序库是Lambda。
    • 泛型编程(Generic programming),覆盖traits classes。
    • 模板元编程(Template metaprogramming,TMP)覆盖一个针对编译器assertions而写的程序库。
    • 数学和数值(Math and numerics),包括有理数、八元数和四元数(octonions and quaternions)、常见的公约数(divisor)和少见的多重运算、随机数。
    • 正确性与测试(Correctness and testing),覆盖隐式模板接口(implicit template interfaces)。
    • 数据结构,覆盖类型安全(type-safe)的unions以及tuple程序库。
    • 语言间的支持(Inter-language support),包括C++和Python之间的无缝互操作性(seamless interoperability)。
    • 内存,覆盖Pool程序库,用来做出高效率而区块大小固定的分配器,以及多变化的智能指针(smart pointers)。
    • 杂项,包括CRC检验、日期和时间的处理、在文件系统上来回移动等等。

以下operator new和operator delete的规则同样适用于operator new[]和operator
delete[]

条款49:了解new-handler的行为

  1. 当operator new无法满足某一内存分配需求时,就会抛出异常。在它抛出异常前,会先调用一个用户指定的错误处理函数,一个所谓的new-handler,为了指定这个函数,用户需要调用set_new_handler,那是声明于的一个标准程序库函数:
    1
    2
    3
    4
    5
    6
    7
    namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
    }

    // 使用时
    std::set_new_handler(outOfMem);
    一个设计良好的new-handler函数必须做以下事情:
    • 让更多内存可被使用。让operator new的下一次内存分配动作可能成功,就让程序一开始执行分配一大块内存,当new-handler第一次被调用,将它们释放给程序使用。
    • 安装另一个new-handler。如果目前这个new-handler无法取得个更多可用内存,或许其他new-handler有。那么这个new-handler则set_new_handler来替换自己。下次当operator new调用new-handler时,调用的将是最新安装的那个。
    • 卸除new-handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常,这时候应该将null指针给set_new_handler。
    • 抛出bad_alloc(或派生自bad_alloc)的异常。这样异常不会被operator new捕捉,会被传播到内存索求处。
    • 不返回。调用abort或exit。
  2. 使用class内自定义的operator new的set_new_handler可以替换global 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
    48
    49
    50
    class NewHandlerHolder
    {
    public:
    // 取得当前的的new-handler
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
    // 释放它
    ~NewHandlerHolder() {
    std::set_new_handler(handler);
    }
    private:
    std::new_handler handler;
    // 禁用copying函数
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& oeprator=(const NewHandlerHolder&);
    }

    class Widget
    {
    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 = 0;
    };
    std::new_handler Widget::set_new_handler(std::new_handler p) throw()
    {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
    }
    void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
    {
    // 安装Widget的new-handler
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    // 恢复global new-handler
    return ::operator new(size);
    }

    // 函数声明,将在Widget对象分配失败时被调用
    void outOfMem();
    // 设定outOfMem为Widget的new-hander函数
    Widget::set_new_handler(outOfMem);
    // 分配失败调用outOfMem
    Widget *pw1 = new Widget;
    // 分配失败调用global new-handler函数
    std::string *ps = new std::string;
    // 设定Widget专属的new-handing函数为null
    Widget::set_new_handler(0);
    // 如果分配失败,立刻抛出异常
    Widget *pw2 = new Widget;
  3. 现在设计一个可以被任何有需要的class使用的template:
    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
    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 oldHandler;
    }
    template<typename T>
    void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
    {
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
    }
    template<typename T>
    std::new_handler NewHandlerSupport<T>::currenHandler = 0;

    // 对于显式声明nothrow,只能保证operator new不抛出异常,构造函数可能又new一些内存,这就无法强迫了
    // Widget *pw = new (std::nothrow)Widget;
    怪异的循环模板模式curiously recurring template pattern;CRTP)。

条款50:了解new和delete的合理替换时机

  1. 为什么要替换编译期提供的operator new或operator delete呢?由三个理由:
    • 用来检测运用上的错误。如果new出的内存delete失败,会导致内存泄露memory leaks)。new上多次delete会导致不确定行为。如果我们自行定义operator news,超额分配内存,在额外内存上放置byte patterns(签名,signatures)。这样delete时就可以发现分配区中有哪些点发生了overrun(写入点在分配区尾端之后)或underruns(写入点在分配区起点之前)。这时候就可以log那个事实了。
    • 为了强化效能。对于一些需求,包括大块内存、小块内存、大小混合型内存。它们接纳各种分配形态,从程序存活期间的少量区块动态分配,到大量短命对象的持续分配和归还。必须考虑破碎问题fragmentation)。这会导致程序有总量足够但分散为很多小区块的自由内存,却无法分配大区块内存。
    • 为了收集使用上的统计数据。制定news和delete之前,先收集软件如何使用动态内存。分配区块的大小分配?寿命分布?FIFO次序还是LIFO次序或随机分配?不同执行阶段有不同分配/归还形态?任何时刻使用最大动态分配量是多少?
  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static const int signature = 0xDEADBEEF;
    typedef unsigned char Byte;
    // 还有若干错误
    void* operator new(std::size_t size) throw(std::bad_alloc)
    {
    using namespace std;
    // 增加大小,使之能够塞入两个signatures
    size_t realSize = size + 2 * sizeof(int);
    void* pMem = malloc(realSize);
    if (!pMem) throw bad_alloc();
    // 将signature写入内存的前段落和后段落
    *(static_cast<int*>(pMem)) = signature;
    *(reinterrpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
    // 返回指针,指向第一个signature之后的内存位置
    return static_cast<Byte*>(pMem) + sizeof(int);
    }
    计算机体系结构computer architectures)要求特定的类型必须放在特定内存地址上,例如指针的地址必须是4倍数for-byte aligned)或doubles地址是8倍数eight-byte aligned)。齐位alignment)意义重大,C++要求operator news返回的指针都有适当的内存对齐。malloc就是在这样做的,所以operator new返回一个malloc指针是安全的。然而上面代码中返回一个malloc且偏移一个int大小的指针,没人保证它的安全!
  3. 摘要:
    • 为了检测运用错误。
    • 为了收集动态分配内存之使用统计信息
    • 为了增加分配和归还速度
    • 为了降低缺省内存管理器带来的空间额外开销。
    • 为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。
    • 为了将相关对象成簇集中。
    • 为了获得非传统的行为。

条款51:编写new和delete时需固守常规

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

条款52:写了placement new也要写placement delete

  1. 1
    2
    3
    4
    5
    6
    // 正常的operator new
    void* operator new(std::size_t) throw(std::bad_alloc);
    // global作用域中的正常签名式
    void operator delete(void *rawMemory) throw();
    // class作用域中典型的签名式
    void operator delete(void *rawMemory, std::size_t size) throw();
    如果operator new接受的参数除了一定会有个size_t之外还有其他,这就是所谓的placement new。众多placement new版本中特别有用的一个是”接受一个指针指向对象该被构造之处“:
    1
    void* operator new(std::size_t, void *pMemory) throw();  // placement new
    这个版本在C++ STL中,只要#include <new>就可以用它。
    举个例子,现在写一个class专属的operator new,要求接受一个ostream,用来log相关分配信息,同时写一个正常形式的class专属operator delete:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Widget
    {
    public:
    ...
    // 非正常形式的new
    static void* operator new(std::size size, std::ostream &logStream) throw(std::bad_alloc);
    // 正常class专属delete
    static void operator delete(void *pMemory, std::size_t size) throw();
    ...
    }
    // 这里有微妙的内存泄漏
    // 调用operator new并传递cerr为其ostream实参,这个动作会在构造函数抛出异常时泄露内存
    Widget *pw = new (std::cerr) Widget;
    对于类似的new placement版本,operator delete如果接受额外参数,称为placement deletes。带有额外参数的operator new应该调用带相同额外参数的对应版本operator delete。
  2. 为了消弭代码中的内存泄露,Widget有必要声明一个placement delete,对应于有志记功能(logging)的placement new:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Widget
    {
    public:
    ...
    static void* operator new(std::size_t size, std::ostream &logStream) throw(std::bad_alloc);
    static void operator delete(void *pMemory) throw();
    static void operator delete(void *pMemory, std::ostream &logStream) throw();
    ...
    }
    对于placement delete自动调用期间构造函数可能会抛出异常,代码会自动调用placement delete;如果没有抛出异常,则只会调用普通operator delete。
    顺带一提,成员函数的名称会掩盖外围作用域中的相同名称,假设有一个base class,其中声明唯一的placement operator new,用户会无法使用正常的new:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base
    {
    public:
    ...
    // 会掩盖正常版本
    static void* operator new(std::size_t size, std::ostream &logStream) throw(std::bad_alloc);
    ...
    };
    class Derived : public Base
    {
    public:
    ...
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
    };
    Base *pb = new Base; // error
    Base *pb = new (std::cerr) Base; // ok
    Derived *pb = new (std::clog) Derived;// error
    Derived *pb = new Derived; // ok
    需要记住的时,C++在global作用域中提供了以下operator new:
    1
    2
    3
    4
    5
    6
    // normal new
    void* operator new(std::size_t) throw(std::bad_alloc);
    // placement new
    void* operator new(std::size_t, void *) throw();
    // nothrow new
    void* operator new(std::size_t, const std::nothrow_t &) throw(); // 49
    除非你要阻止class用户使用这些形式,否则确保它们在你生成的定制型operator new之外还可用。对于每个operator new也有对应的operator delete。如果希望这些函数有平常的行为,令你class专属版本调用global版本:
    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
    class StandardNewDeleteForms
    {
    public:
    // normal new/delete
    static void* operator new(std::size_t size) throw(std::bad_alloc)
    { return ::operator new(size); }
    static void operator delete(void *pMemory) throw()
    { ::operator delete(pMemory); }

    // placement new/delete
    static void* operator new(std::size_t size, void *ptr) throw()
    { return ::operator new(size, ptr); }
    static void operator delete(void *pMemory, void *ptr) throw()
    { return ::opeator delete(pMemory, ptr); }

    // nothrow new/delete
    static void* operator new(std::size_t size, const std::nothrow_t &nt) throw
    { return ::operator new(size, nt); }
    static void operator delete(void *pMemory, const std::nothrow_t &) throw()
    { ::operator delete(pMemory); }
    };

    // 供想自定义扩充标准形式的用户,利用继承机制或using声明式(33)取得标准形式
    class Widget : public StandardNewDeleteForms
    {
    public:
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    static void* operator new(std::size_t size, std::ostream &logStream) throw(std::bad_alloc);
    static void oeprator delete(void *pMemory, std::ostream &logStream) throw();
    ...
    };

C++
templates的最初发展动机很直接:让我们得以建立type-sfae的容器(vector、list、map等)。后来发现templates有能力完成愈多可能的变化。泛型编程generic
programming
)——写出的代码和其所处理的对象类型彼此独立(for_each、find、merge等)。最终人们发现C++
template机制自身是完整的图灵机Turing-complete)。于是导出模板元编程template
metaprogramming
),创造出在C++编译器内执行并于编译完成时停止执行的程序。

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

  1. 面向对象编程世界总是以显示接口explicit interfaces)和运行期多态runtime polymorphism)解决问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Widget 
    {
    public:
    Widget();
    virtual ~Widget();
    virutal std::size_t size() const;
    virtual void normalize();
    void swap(Widget &other);
    ...
    }
    void doProcessing(Widget &w)
    {
    if (w.size() > 10 && w != someNastyWidget)
    {
    Widget temp(w);
    temp.normalize();
    temp.swap(w);
    }
    }
    template及泛型编程的世界,显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口implicit interfaces)和编译期多态compile-time polymorphism)移到前头。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typename T>
    void doProcessing(T &w)
    {
    if (w.size() > 10 && w != someNastyWidget)
    {
    T temp(w);
    temp.normalize();
    temp.swap(w);
    }
    }
    这组表达式便是T必须支持的一组隐式接口(implicit interface)。凡涉及w相关的函数调用,如operator>和operator!=,有可能造成template具体化instantiated),使这些调用成功,这样的具现行为发生在编译期,这就是所谓的编译期多态(compile-time polymorphism)。
  2. 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42:了解typename的双重意义

  1. template内出现的名称如果相依于某个template参数,称之为从属名称dependent names)。如果从属名称在class内呈嵌套状,称之为嵌套从属名称nested dependent name),也就是嵌套从属名称并且指涉某类型;否则一个并不依赖任何template参数的名称。这样的名称是非从属名称non-dependent names)。
  2. 任何当你想要在template中指涉一个嵌套嵌套类型名称,必须在它的前一个位置放上关键字typename。这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list中作为base class修饰符。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // base class list中不允许typename
    template<typename T>
    class Derived : public Base<T>::Nested
    {
    public:
    // mem init list中不允许typename
    expliit Derived(int x) : Base<T>::Nested(x) //
    {
    // 既不在base class list中也不在mem init list中,需要加上typename
    typename Base<T>::Nested temp;
    ...
    }
    ...
    };

    template<typename IterT>
    void workWithIterator(IterT iter)
    {
    // traits class相当于类型为IterT对象所指对象的类型,如果IterT是list<string>::iterator, temp的类型就是string
    typename std::iterator_traits<IterT>::value_type temp(*iter);
    ...
    }

条款43:学习处理模板化基类内的名称

  1. 以下代码编译时出错:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename T>
    class base
    {
    public:
    void printBase() { std::cout << "base" << std::endl; }
    };
    template<typename T>
    class derived : public base<T>
    {
    public:
    void printDerived()
    {
    std::cout << "derived" << std::endl;
    printBase();
    }
    };
    问题在于当编译器遭遇class template derived定义式时,并不知道它继承什么样的class,它不知道T是个template参数,不到后来具现化无法确切知道它是什么。而不知道T是什么,也就不知道class base看起来像什么。
  2. 针对某个类产生一个特化版,需要在class定义式最前头加上“template<>”。这就是所谓的模板全特化total template specialization)。对于某个被特化的base class templates,那个特化版本可能不提供和一般性template相同的接口,因此它往往拒绝在templatized base classes模板化基类)内寻找继承而来的名称。就某种意义上来说,从Object Oriented C++跨进Template C++就不是畅通无阻了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // example
    template<>
    class A<int>
    {
    public:
    printBase()
    {
    std::cout << "A::int special" << std::endl;
    }
    };
  3. 为了进入templatized base classes,有三个办法:
    • 第一是在调用base class函数动作之前加上“this->”
      1
      this->printBase();
    • 第二是使用using声明式。
      1
      using A<T>::printBase();
    • 第三种是明白指出调用的函数位于base class内。
      1
      A<T>::printBase();
      这是最不让人满意的揭发,如果被调用的是virtual函数,上述的明确资格修饰explicit qualification)会关闭“virtual绑定行为”。

条款44:将与参数无关的代码抽离templates

  1. 举个例子,用固定尺寸的正方矩阵编写一个template:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // n * n矩阵
    tepmlate<typename T, std::size_t n>
    class SquareMatrix
    {
    public:
    ...
    // 求逆矩阵
    void invert();
    };
    SquareMatrix<double, 5> sm1;
    sm1.invert();
    SquareMatrix<double, 10> sm2;
    sm2.invert();
    这个template接受一个类型参数T,还接受一个size_t的参数,这是非类型参数non-type parameter)。这些函数并非完全相同,除了常量5和10的区别外完全相同,这就是template引出代码膨胀的经典例子。改进方式:
    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
    template<typename T>
    class SquareMatrixBase
    {
    protected:
    SquareMatrixBase(std::size_t n, T *pMem) : size(n), pData(pMem) {}
    void setDataPtr(T *ptr) { pData = ptr; }
    ...
    private:
    std::size_t size;
    T *pData;
    }

    template<typename T, std::size_t n>
    class SquareMatrix : private SquareMatrixBase<T>
    {
    public:
    SquareMatrix() : SquareMatrixBase<T>(n, data) {}
    ...
    private:
    T data[n * n];
    }

    // 动态分配办法
    template<typename T, std::size_t n>
    class SquareMatrix : private SquareMatrixBase<T>
    {
    public:
    SquareMatrix() : SquareMatrixBase<T>(n, 0), pData(new T[n * n])
    { this->setDataPtr(pData.get()); }
    ...
    private:
    boost::scoped_array<T> pData; // std::array??
    }
  2. 这个条款讨论的是non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。因此凡templates持有指针者应该对每个成员函数使用唯一一份底层实现。这意味着实现某些成员函数操作强型指针strongly typed pointers,即T*),令它们调用无类型指针untyped pointers,即void*)的函数。
  3. 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述binary representations)的具体类型instantiation types)共享实现码。

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

  1. 智能指针(Smart pointers)是“行为像指针“的对象,提供指针没有的机能。真实的指针支持隐式转换(inplicit conversions)。而用户自定义的智能指针模拟转换有点麻烦:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Top { ... };
    class Middle : public Top { ... };
    class Bottom : public Middle { ... };

    template<typename T>
    class SmartPtr
    {
    public:
    // 以other的heldPtr初始化this的heldPtr
    tempalte<typename U>
    SmartPtr(const SmartPtr<U> &oher) : heldPtr(other.get()) { ... }
    T* get() const { return heldPtr; }
    ...
    private:
    T* heldPtr;
    };
    根据对象u创建对象t,根据SmartPtr<U>创建SmartPtr,同一个template不同具现体称为泛化(generalized)copy构造函数。以上使用member initialization list来初始化SmartPtr<t>内类型为T的成员变量,并以类型为U的指针为初值。
  2. 成员函数模板作用不限于构造函数。TR1的shared_ptr支持所有兼容它的内置指针、shared_ptrs、auto_ptrs、week_ptrs的构造行为以及赋值作用:
    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
    template<class T>
    class shared_ptr
    {
    public:
    // copy构造函数
    shared_ptr(shared_ptr const &r);
    // copy assignment
    shared_ptr& operator=(shared_ptr const &r);

    // 任何兼容的内置指针
    template<class Y>
    explicit shared_ptr(Y *p);

    // shared_ptr
    template<class Y>
    shared_ptr(shared_ptr<Y> const &r);

    // week_ptr
    template<class Y>
    explicit shared_ptr(week_ptr<Y> const &r);

    // auto_ptr
    template<classs Y>
    explcit shared_ptr(auto_ptr<Y> &r);

    // 赋值
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const &r);
    template<class Y>
    shared_ptr& operator=(auto_ptr<Y> &r);
    ...
    }
    在class内声明泛型copy构造函数(member template)并不会阻止编译期生成它们自己的copy构造函数(一个non-template)。

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

  1. 参考条款24:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template<typename T>
    class Rational
    {
    public:
    Rational(const T &numerator = 0, const T &denominator = 1);
    const T numerator() const;
    const T denominator() const;
    ...
    };
    template<typename T>
    const Rational<T> operator*(const Rational<T> &lhs, const Rational<T> &rhs)
    { ... }

    Rational<int> oneHalf(1, 2); // 这个例子来自条款24
    Rational<int> result = oneHalf * 2; // 错误,无法通过编译。
    它给我们的启示是,模板化的Rational内的某些东西似乎和其non-template版本不同。编译期不知道它要调用哪个函数。operator*的第一个参数被声明为Rational<T>,而传递给operator*的的第一个实参的类型是Rational<int>,所以T是int。operator*的第二参数被声明为Rational,但传递给operator*的第二实参的类型是int。你期望编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,但它不这么做。template实参推导过程中并不考虑采纳”通过构造函数而发生的“隐式类型转换。
  2. 只要利用一个事实,可以缓和编译器在template实参推导方面受到的挑战:template class内的friend声明式可以指涉某个特定函数。class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational具现化时得知T。令Rational class声明适当的operator*为其friend函数,可简化整个问题:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename T>
    class Rational
    {
    public:
    ...
    // 为了让这个函数自动具现化,在class内声明non-member函数的唯一方法是让它称为一个friend
    friend const Rational operator*(const Rational &lhs, const Rational &rhs)
    {
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominaotr());
    }
    };

    // 为了让类型转换可能发生于所有实参身上,其实这个函数可以当作friend函数的辅助函数
    template<typename T>
    const Rational<T> operator*(const Rational<T> &lhs, const Rational<T> &rhs)
    { ... }
    现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,class Rationao<int>就被具化出来,friend函数也就被自动声明出来(这里的Rational<T>可以省略)。后者身为一个函数而非函数模板,因而编译器在调用它时使用隐式转换函数。

条款47:请使用traits classes表现类型信息

  1. STL迭代器有5种分类,每类迭代器之间都是继承关系:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // input迭代器只能向前移动,一次一步,只可读取,只能读一次。如istream_iterator
    struct input_iterator_tag {};
    // output迭代器一切只为输出,它向前移动,一次一步,只能涂写一次。如ostream_iterator
    struct output_iterator_tag {};
    // 这两类只能向前移动,而且只能读或写一些,它们只适合”一次性操作算法“(one-pass algorithms)。

    // forward迭代器可以读或写多次,可施行于多次行操作算法(multi-pass algorithms)。
    struct forward_iterator_tag : public input _iterator_tag {};

    // bidirectional迭代器除了可以向前移动,还可向后移动(set、multiset、map和multimap
    struct bidirectional_iterator_tag : public forward_iterator_tag {};

    // random access迭代器除了可以向前向后,还可随机访问
    struct random_access_iterator_tag : public bdirectional_iterator_tag {};
  2. STL中有个工具性template,叫advance,用来将某个迭代器移动若干给定距离:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 所有工作都运行期,这这个typeid-based解法比traits解法效率低
    // 48条款
    templaet<typename IterT, typename DistT>
    void advance(IterT &iter, DistT d)
    {
    if (iter i a random access iterator) {
    iter += d;
    } else {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
    }
    }
    为了处理迭代器分类的相关信息,我们需要traits来在编译期间取得某些类型信息,而traits技术必须对内置built-in)类型和用户自定义user-defined)类型一样有效运行。这样的template在STL中有若干个,其中针对迭代器的有iterator_traits
    它的运作方式是,它首先要求每个用户自定义的迭代器类型嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构tag struct)。
    例如,针对deque和双向list:
    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
    template< ... >
    class deque
    {
    public:
    class iterator
    {
    public:
    typedef random_access_iterator_tag iterator_category;
    ...
    };
    ...
    };

    template< ... >
    class list
    {
    public:
    class iterator
    {
    public:
    typedef bidirectional_iterator_tag iterator_category;
    ...
    };
    ...
    };

    // iterator_traits只是鹦鹉学舌
    template<typename IterT>
    struct iterator_traits
    {
    typedef typename IterT::iterator_category iterator_category;
    ..
    };
    这对用户自定义类型行得通,对指针(也是一种迭代器)行不通。为了支持指针迭代器,iterator_traits针对指针类型提供了一个偏特化版本partial template specialization):
    1
    2
    3
    4
    5
    6
    7
    // iterator_traits的偏特化版本
    template<typename IterT>
    struct iterator_traits<IterT*>
    {
    typedef random_access_iterator_tag iterator_category;
    ...
    };
    有了std::iterator_traits后,可以对advance践行之前的伪码了,而为了接受不同类型的iterator_category对象,则需要重载overloading):
    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
    // random access迭代器
    template<typename IterT, typenmae DistT>
    void doAdvance(IterT &iter, DistT d, std::random_access_iterator_tag)
    {
    iter += d;
    }

    // bidirectional迭代器
    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; }
    }

    // input迭代器
    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;
    }

    /* 有了doAdvance重载版本,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))
    */
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
    }
  3. STL中除了Iterator_traits供应iterator_category还供应另外四种迭代器:value_typechar_traitsnumeric_limits等。STL中还有很多其他新的traits classes用以提供类型信息,如is_fundamental<T>(判断T是否为内置类型),is_array<T>(判断T是否为数组类型)以及is_base_of<T1, T2>(T1和T2相同,或T1是T2的base class)。

条款48:认适template元编程

  1. Template metaprogramming(TMP模板元编程)是编写template-based C++程序并执行于编译器的过程。
  2. 47条款中的伪码部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 面对某些自定义类型,会编译失败
    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)) {
    iter += d;
    } else {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
    }
    }
  3. TMP已被证明是”图灵完全“(Turing-complete)机器,针对TMP而涉及的程序库提供了更高级的语法。TMP并没有真正的循环结构,所有的循环效果都是由递归recursion)完成。TMP主要是个函数时语言(functional language),而递归在这类语言是无法分割的,TMP循环并不涉及递归函数调用,而是涉及”递归模板具现化“(recursive template instantiation)。以下是示例源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 一般情况下
    template<unsigned n>
    struct Factorial {
    enum { value = n * Factorial<n - 1>::value };
    };
    // 特殊情况下,当到0时
    template<>
    struct Factorial<0> {
    enum { value = 1 };
    };
  4. TMP能够达到什么目标呢:
    1. 确保度量单位正确。在科学和工程应用程序中,可以用TMP确保在编译期时确保所有度量单位组合正确,防范于未然。
    2. 优化矩阵运算。
    3. 生成用户定制之设计模式(custom design pattern)实现品。这项技术已被用来让若干templates实现出智能指针的行为策略(behavioral policies),用来在编译期间生成数以百计不同的智能指针类型。这项技术超越编程工艺如设计模式和智能指针,更广义成为generative programming殖生式编程)的一个基础。

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

  1. public inheritance(公开继承)意味”is-a“(是一种)的关系。
  2. 令class D(”Derived”)以public形式继承class B(“Base”),意味着每个类型为D的对象同时也是一个类型为B的对象,反之不成立。B比D概念上更一般化,D比B更特殊化。
  3. 对于现实世界,我们可能对已有事物进行概念上的内涵和外延来抽象不同的状态和动作。企鹅是一种鸟,然而它不会飞,对于鸟会飞的先入为主的观念,可能会使实际偏颇。“企鹅不会飞”这一限制可由编译期强制实施,但若违反“企鹅尝试飞行,是一种错误”这一条规则,只有运行期才能检测出来。
  4. is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。

条款33:避免遮掩继承而来的名称

  1. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数orwarding functions)。

条款34:区分接口继承和实现继承

  1. public继承概念由两部分组成:函数接口function interfaces)继承和函数实现function implementations)继承。
  2. 声明一个pure virtual函数的目的式为了让derived classes只继承函数接口。声明纯朴的impure virtual函数的目的,是让derived classes函数继承改函数的接口和缺省实现。声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

条款35:考虑virtual函数以外的其他选择

  1. 令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是Template Method设计模式的独特表现形式。
  2. Strategy设计模式简单应用:
    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    // NVI写法
    class GameCharacter
    {
    public:
    int healthValue() const
    {
    ...
    int retVal = doHealthValue();
    ...
    return retVal;
    }
    ...
    private:
    virtual int doHealthValue() const
    {
    ...
    }
    };

    // Function Pointers写法
    class GameCharacter; // forward declaration
    int defaultHealthCalc(const GameCharacter &gc);
    class GameCharacter
    {
    public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
    int healthValue() const
    { return healthFunc(*this); }
    ...
    private:
    HealthCalcFunc healthFunc;
    };

    // std::fucntion写法
    // 与FP写法不同的是,std::function对象相当于一个指向函数的泛化指针。
    class GameCharacter;
    int defaultHealthCalc(const GameCharacter &gc);
    class GameCharacter
    {
    public:
    typedef std::function<int (const Characater&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
    int healthvalue() const
    { return healthFunc(*this); }
    ...
    private:
    HealthClacFunc healthFunc;
    };

    // 古典设计模式写法
    class GameCharacter;
    class HealthCalcFunc
    {
    public:
    ...
    virtual int calc(const GameCharacter &gc) const
    { ... }
    ...
    };
    HealthCalcFunc defaultHealthCalc;
    class GameCharacter
    {
    public:
    explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc) : pHealthCalc(phcf) {}
    int healthvalue() const
    { return pHealthcalc->calc(*this); }
    ...
    private:
    HealthCalcFunc *pHealthcalc;
    };
    ```C++
    // 有着更惊人的弹性

// 健康计算函数
short calchealth(const GameCharacter&);
// 为计算健康而设计的函数对象
struct HealthCalclator
{
int operator()(const GameCharacter&) const
{ … }