• 【读书笔记】【More Effective C++】技术(Techniques,Idioms,Patterns)


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

    • 【本条款的多个实现都比较相似:写一个虚函数做实际工作,再一个非虚函数调用该虚函数】
    • 原则上构造函数不能为虚:
      • 虚函数用于实现因类型而异的行为,也就是根据指针或引用所绑定对象的动态类型而调用不同实体;
      • 构造函数用于构造对象,在对象构造之前自然没有动态类型的概念,虚与非虚也就无从谈起。
    • 所谓的 virtual constructor 实际上是仿 virtual constructor,它本质上不是 constructor,但能够产生不同类型的对象,从而实现 virtual constructor 的功能。
    • 构造函数虚拟化技术:
      • 代码实现:

        class NLComponent {// 抽象基类,其中内含至少一个纯虚函数
        public:
        ...
        };
        
        class TextBlock: public NLComponent{// 没有内含任何纯虚函数
        public:
        ...
        };
        
        class Graphic: public NLComponent{// 没有内含任何纯虚函数
        public:
        ...
        };
        
        class NewsLetter { // 一份实时通信是由一系列的NLComponent对象构成的
        public:
        NewsLetter(istream& str);// NewsLetter拥有一个istream为自变量
        // 的构造函数,函数由stream读取数据以便产生必要的核心数据结构
        ...
        private:
        // readComponent 从str读取下一个NLComponent的数据,产生组件,并返回一个指针指向它
        static NLComponent* readComponent(istream &str); // 要注意这里返回的是base类指针
        list<NLComponent *> components;
        };
        
        NewsLetter::NewsLetter(istream &str) // 构造函数
        {
            while(str)
            {// 将readComponent返回的指针加到Component list尾端
                components.push_back(readComponent(str));
            }
        }
        
        • 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
      • 类的彼此关系如下:
        类的彼此关系

      • 由以上代码可知:

        1. NLComponent 的构造函数并没有虚拟化,只是通过 readComponent 函数读取 istream 产生不同类型的组件,返回组件的指针;
          • 之后将指针以基类指针(NLComponent*)类型存储起来,用来在以后的调用可以实现多态,这样就是 virtual constructor。
        2. NewsLetter 类的 readComponent 函数根据输入的字符串不同产生不同的对象;它产生新对象,所以行为好像 constructor,但它能够产生不同类型的对象,所以称为一个 virtual constructor。
          • 所谓 virtual constructor 是指能够根据输入给它的数据的不同而产生不同类型的对象。
    • copy 构造函数虚拟技术:
      • 代码实现:
        class NLComponent {
        public:
            // 声明virtual copy constructor
            virtual NLComponent * clone() const = 0;
            ...
        };
        class TextBlock: public NLComponent {
        public:
            virtual TextBlock * clone() const // virtual copy constructor
            { return new TextBlock(*this); } // 返回该对象的副本
            ...
        };
        class Graphic: public NLComponent {
        public:
            virtual Graphic * clone() const // virtual copy constructor
            { return new Graphic(*this); } 
            ...
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
      • 这里的 clone 就实现了 virtual copy constructor 的功能,clone 只是调用真正的 copy constructor 而已。
      • 当子类重新定义其基类的一个虚函数时,不需要一定得声明与原本相同的返回类型。【即重写了函数】
      • 这个技术其实是有助于补充完整前面的 NewsLetter 实现,因为前面的技术可以这样子实现了:
        class NewsLetter {
        public:
            NewsLetter(const NewsLetter& rhs);
            ...
        private:
            list<NLComponent*> components;
        };
        NewsLetter::NewsLetter(const NewsLetter& rhs){
            //遍历rhs的list,运用每个元素的virtual copy constructor将元素复制到此对象的compnents list中.
            for (list<NLComponent*>::const_iterator it =rhs.components.begin();it != rhs.components.end();++it)
                //it指向rhs.compnents的目前元素,调用该元素的clone函数取得一个副本并加到本对象的compnents list的尾端
                components.push_back((*it)->clone());
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
    • 非成员函数虚拟化技术:
      • 正如 constructors 无法被虚化,non-member function 原则上也无法被虚化:因为它连成员函数都不是。
      • 实现上其实就是在外部创建一个非成员函数,而这个非成员函数调用类内的虚函数来运作
        #include
        using namespace std;
        
        class NLComponent{
        public:
            virtual ostream& print(ostream& s) const = 0;
        };
        class TextBlock:public NLComponent{
        public:
            virtual ostream& print(ostream& s) const // 虚函数实现
            {
                s << "TextBlock";
                return s;
            }
        };
        class Graphic : public NLComponent{
        public:
            virtual ostream& print(ostream& s) const
            {
                s << "Graphic";
                return s;
            }
        };
        inline ostream& operator<<(ostream& s, const NLComponent& c)
        {
            return c.print(s);
        }
        
        int main(){
            TextBlock tx;
            Graphic gc;
            cout << tx << endl;
            cout << gc << endl;
            return 0;
        }
        // 这里的print函数是虚函数
        // 再重载一个operator<<来调用print
        
        • 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

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

    • 本条款讨论引用计数思想:建立一个基类,构造函数和复制构造函数中计数加一,若超过最大值则抛出异常;析构函数中计数减一。
    • 本条款在实现细节上也是值得关注的,学习这种将方案步步推进的思路。
    • 首先,考虑如何允许零个或一个对象的产生:【单例模式】
      • 方案一:【将构造函数声明为 private,同时利用函数中的 static 变量确保最多只有一个对象生成】【在函数第一次被调用时,对象才生成】
        // 方案一的第一版:【友元函数版本】
            // 将构造函数设为private,
            // 然后声明一个友元函数调用它,
            // 并生成一个static对象
        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(){// 要使用Printer class的时候,就调用thePrinter
            static Printer p;// static保证了只产生一个Printer对象
            return p;
        }
        
        // 方案一的第二版:【成员函数版本】
            // 在这版中,static的Printer成为了类内的static成员
            // 第一版的“函数中的static对象”保证了在第一次调用函数时才被初始化
        class Printer {
        public:
            static Printer& thePrinter();
            ...
        private:
            Printer();
            Printer(const Printer& rhs);
            ...
        };
        Printer& Printer::thePrinter(){
            static Printer p;
            return p;
        }
        // 注意带有局部 static 函数不要转为 inline
        
        • 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
      • 方案二:【用一个变量记录当前的对象个数,当外界申请太多对象时,在构造函数内抛出异常】
        class Printer {
        public:
            class TooManyObjects{}; // 异常
            Printer(); // 构造函数是 public 的
            ~Printer();
            ...
        private:
            static size_t numObjects;
            Printer(const Printer& rhs);//由于只允许产生一个对象,所以不允许拷贝
        };
        size_t Printer::numObjects = 0;
        Printer::Printer(){
            if (numObjects >= 1) {
                throw TooManyObjects();
            }
            // proceed with normal construction here;
            ++numObjects;
        }
        Printer::~Printer(){
            // perform normal destruction here;
            --numObjects;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
    • 方案一带来的问题在于:它确实限制了 Printer 对象个数为 1,但同样它限制了在每次执行程序时只能有唯一的 Printer 对象
      • 也就是说,当 Printer 对象第一次创建,然后被销毁,第二次创建就会出现问题,这操作并没有违反“只有一个 Printer”的条件,但方案一的实现使得这种非程序员本意的局面出现了。
    • 方案二更容易被一般化,即可以使对象数目限制在除 1 以外的值,但是方案二在涉及到类的继承以及内含的情况下不起作用
      • 因为 Printer 对象可于三种不同状态下生存:它自己、派生物的 base class 成分、内嵌于较大对象之中。【其他情况都不可以】
        // 涉及继承:
        class ColorPrinter: public Printer {
            ...
        };
        Printer p;
        ColorPrinter cp; // 抛出异常
        
        // 涉及内含:
        class CPFMachine { 
        private:
            Printer p; 
            FaxMachine f; 
            CopyMachine c;
            ...
        };
        CPFMachine m1; 
        CPFMachine m2; // 抛出异常
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
      • 方案二使得 base 类的个数限制在 1 个,同时其继承类和包含类的个数也受到了限制,这并非原意。
      • 方案一不存在这类问题,因为将构造函数设为 private 实际上禁止了继承和内含的发生。
    • 现在看来,方案二的问题就是如何避免 class 被继承,看看另外一种情况,假设希望某个 class 可以产生任意数量的对象,但也希望确保没有任何继承类继承自该类
      • 可以用以下方案来解决该问题:【伪构造函数:通过开放的接口提供对象引用或指针
        class FSA {
            public:
                static FSA* makeFSA(); // 伪构造函数
                static FSA* makeFSA(const FSA& rhs); // 伪拷贝构造函数
            private: // 将真正的构造函数设为 private
                FSA();
                FSA(const FSA& rhs);
        }
        FSA* FSA::makeFSA()
        { return new FSA();}
        FSA* FSA::makeFSA(const FSA& rhs)
        { return new FSA(rhs);}
        
        // 使用如下:为了保证没有资源泄露,可以使用 auto_ptr
        auto_ptr<FSA> pfsal(FSA::makeFSA());
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
    • 有了伪构造函数的技术,就可以将方案二进行完善了。【对象计数 + 将构造函数 private + 给外界提供伪构造函数接口
      • 示例如下:
        // 定义如下:
        class Printer {
        public:
            class TooManyObjects{};
            static Printer * makePrinter(); // 伪构造函数
            static Printer * makePrinter(const Printer& rhs); // 伪拷贝构造函数
            ...
        private:
            static size_t numObjects;
            static const size_t maxObjects = 10; // 对象的数目限制,也可以使用枚举
            Printer();
            Printer(const Printer& rhs);
        };
        
        // 实现如下:
        size_t Printer::numObjects = 0;
        const size_t Printer::maxObjects;
        Printer::Printer(){
            if (numObjects >= maxObjects) {
                throw TooManyObjects();
            }
            ...
        }
        Printer::Printer(const Printer& rhs){
            if (numObjects >= maxObjects) {
                throw TooManyObjects();
            }
            ...
        }
        Printer * Printer::makePrinter(){ // 伪构造函数
            return new Printer; 
        }
        Printer * Printer::makePrinter(const Printer& rhs){ // 伪拷贝构造函数
            return new Printer(rhs); 
        }
        
        • 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
    • 在最后,将计数器模板化
      • 任何需要限制对象数目的类只要继承这个模板的实例化即可:
        template<class BeingCounted>
        class Counted {
        public:
            class TooManyObjects{}; 
            static int objectCount() { return numObjects; }
        protected:
            Counted();
            Counted(const Counted& rhs);
            ~Counted() { --numObjects; }
        private:
            static int numObjects;
            static const size_t maxObjects;
            void init(); 
        }; 
        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;
        }
        
        • 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
      • 前面的 Printer 要使用 Counted 就如下所示:
        class Printer: private Counted<Printer> {
        // Printer的其他内容都和上一部分内容一样
        // 即保持将真正构造函数 private 和提供伪构造函数接口
        public:
            // ...
            using Counted<Printer>::objectCount; // 函数,返回当前计数
            using Counted<Printer>::TooManyObjects; // 异常
            // ...
        private:
            // ...
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
    • 在最后的最后,唯一要讨论的细节是最后的方案中 static 成员的定义问题:【Counted 类模板使用了 static 成员】
      • static 成员必须要在类外定义;
      • 对于 numObjects 可以在头文件中初始化为 0;
      • 用于限制对象个数的 maxObjects 只能由用户定义并初始化,如果用户忘记定义,链接时将会报错并提醒。【因为 maxObjects 不好由类设计者提供初始化】

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

    • 要求对象产生于堆中
      • 在原来,创建栈上的对象必然需要调用构造函数和析构函数;因此,我们要求对象产生于堆中,其实就是禁止对象产生于栈中。【要求对象产生于堆中 = 要求对象不能以其他形式产生 = 禁止对象产生于栈中 = 使调用构造函数和析构函数的动作不合法】
      • 解决方法有两种:第一种,将所有的构造函数声明为 private;第二种,将唯一的析构函数声明为 private。【无需将构造函数和析构函数均设置为 private】
        • 要注意这是两种不同的方案,又因为析构函数只有一个,所以一般将析构函数声明为 private。
        • 将析构函数声明为 private 的同时,public 中需要有一个相应的 pseudo destructor(伪析构函数),它是一个 const member function,因为 const 对象也需要被销毁。【这个伪析构函数内部就只是 delete 了元素,其实就是说如果采用了这种方法,用户就无法配套使用 new 和 delete,而要使用 new 和该伪析构函数】
      • 但当我们把构造函数或析构函数置为 private 的时候,同时也阻止了继承和内含。【因为继承或内含的那个 class 其构造函数和析构函数不能通过编译】
        • 想要保留继承功能:可以把权限扩大,即把基类的析构函数置为 protected,基类的构造函数置为 public
        • 想要保留内含功能:修改为内含一个指针,而指针指向对象。
    • 判断某个对象是否位于堆中
      • 前面提出的方法依然不能解决在继承情况下基类可能位于 non-heap 的问题。【也就是说,如何知道一个继承类的基类部分是否位于 heap?】
      • 本条款提出了几个策略来判断,但各自都有问题。
      • 策略一:重载 operator new,让 new 操作符调用新的那个 operator new。
        class UPNumber {
        public:
        // 如果产生一个非堆对象,就抛出异常
            class HeapConstraintViolation {};
            static void * operator new(size_t size);
            UPNumber();
            ...
        private:
            static bool onTheHeap;// 标志对象是否被构造于堆上
            ... 
        };
        // 类外部定义静态成员
        bool UPNumber::onTheHeap = false;
        void *UPNumber::operator new(size_t size)
        {
            onTheHeap = true;// 调用一次new,就会更新标志位
            return ::operator new(size);
        }
        UPNumber::UPNumber()
        {
            if (!onTheHeap) {
                throw HeapConstraintViolation();
            }
            // proceed with normal construction here;
            onTheHeap = false;// 清除flag
        }
        
        • 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
        • 对上面的代码进行分析:这种方法对于产生单个对象的确可行,用户如果通过 new 来产生对象,onTheHeap 就会在 operator new 中被设为 true,构造函数被正常调用;如果对象不是产生于堆中,onTheHeap 就为 false,调用构造函数时就会抛出异常。
        • 但对于数组的产生,该策略存在硬伤。
          • 原因其实也很直观,operator new[] 第一次调用构造函数时将 onTheHeap 置为 true,以后将不再调用 operator new[];所以数组中的元素在第二次调用构造函数时候就抛出了异常。
          • 或者再添加一个 static bool 标记,用于标记对象是否是作为数组的元素而产生,但这样也更为复杂,且容易出错。
        • 即使没有数组的问题,接下来的语句仍然有可能出错:
          UPNumber *pn = new UPNumber(*new UPNumber);
          // 暂且不理睬资源泄露的问题
          
          // 这里可能产生的问题是:
              // 通常情况下,我们认为:
              // 先完成第一个对象的new操作和调用构造函数操作,
              // 紧接着完成第二个对象的new操作和调用构造函数操作。
          
              // 但编译器有可能会产生下面的函数调用顺序:
              // 1.为第一个对象调用operator new
              // 2.为第二个对象调用operator new
              // 3.为第一个对象调用constructor
              // 4.为第二个对象调用constructor
          // 这时候策略一就会失效。
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
      • 策略二:利用 stack(栈)高地址往低地址成长,heap(堆)往低地址成长的这个事实。
        bool onHeap(const void *address){
            char onTheStack; // 局部栈变量
            return address < &onTheStack; // 比较 address 和 局部栈变量 的地址
        }
        
        • 1
        • 2
        • 3
        • 4
        • 该策略的缺点很明显:
          • 不具备移植性,因为有的系统是这样分布,有的系统却不是这样。
          • 同时策略二无法区分 heap 对象和 static 对象:static 对象(包括 global scope 和 namespace scope)既不是位于 stack 也不是位于 heap 中,它的位置视系统而定。
      • 【分析了前面两个策略之后,可以知道:没有通用且有效的方法去区分 heap 和 stack 对象】
        • 但可以把这个目标转变为:判断对一个指针使用 delete 是否安全。【当然对象是否位于 heap 中指针是否可以被 delete 并不完全等价】
        • 不等价的原因如下:
          struct Asset{ // 包含关系
              int a;
              UPNumber value;
              ...
          }
          Asset* a(new Asset);
          UPNumber* ptr=&(a->value);
          // 尽管ptr指向的是heap内存,但对ptr实行delete会出错
              // a是通过new取得,但它的成员value并不是通过new取得
              // 对一个指针使用delete是否安全,并不取决于对象是否位于heap中,
              // 本质上取决于它是否位于申请的一段heap内存的开始处
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
      • 策略三:判断对指针 delete 是否安全。【具体实现中,是将由动态分配来的地址加入到一个表中,只有地址在表中,delete 才安全】
        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); // 归还内存
            // remove ptr from the collection of allocated addresses;// 从表中移出来
        }
        bool isSafeToDelete(const void *address)// 判断地址是否在表中
        {
            return whether address is in collection of allocated addresses;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 本策略存在三个缺点:
          1. 可以看到本策略的实施需要重载 operator new 和 operator delete,这往往是应该避免的,因为这会抢夺全局版本的权限。
          2. 需要维护一个表,这会消耗资源。
          3. 最后的问题处在 isSafeToDelete 函数,它未必总是有效;如果对象涉及多重继承或虚拟继承的基类时,会拥有多个地址(不同基类在对象中有不同的地址),因此不能保证交给 isSafeToDelete 的地址被 operator new 返回的地址是同一个:
            class Base1{
            public:
                virtual ~Base(){}
                ...
            private:
                int a;
            }
            class Base2{
            public:
                virtual ~Base2(){}
                ...
            private:
                int b;
            }
            class Derived:public Base1,public Base2{}
            Base2* ptr = new Derived;
            // ptr所指地址不在所维护的表中,因此isSafeToDelete返回false
            // 但对ptr使用delete却是安全的,因为Base2的析构函数为虚
            
            • 1
            • 2
            • 3
            • 4
            • 5
            • 6
            • 7
            • 8
            • 9
            • 10
            • 11
            • 12
            • 13
            • 14
            • 15
            • 16
            • 17
            • 18
      • 策略四:使用 mixin 模式,设计一抽象基类,用于提供一组定义完好的能力。
        • 代码实现如下:【这个基类就是为了后续继承类能判断指针是否以 operator new 分配出来】
          class HeapTracked { 
          public: 
              class MissingAddress{}; // 异常类
              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;//维护heap地址的表
          }list<RawAddress> HeapTracked::addresses;
          
          // 析构函数设为纯虚函数以使得该类成为抽象类,但必须有定义.
          HeapTracked::~HeapTracked() {}
          void * HeapTracked::operator new(size_t size)
          {
              void *memPtr = ::operator new(size); 
              addresses.push_front(memPtr); // 在表中插入新地址
              return memPtr;
          }
          void HeapTracked::operator delete(void *ptr)
          {
              //查找是否在表中
              list<RawAddress>::iterator it =find(addresses.begin(), addresses.end(), ptr);
              if (it != addresses.end()) { 
                  addresses.erase(it); 
                  ::operator delete(ptr); 
              } 
              else {
                  throw MissingAddress(); 
              } 
          }
          bool HeapTracked::isOnHeap() const
          {
              // 得到一个指针,指向*this占据的内存空间的起始处,
              const void *rawAddress = dynamic_cast<const void*>(this);
              // 在表中查找
              list<RawAddress>::iterator it =find(addresses.begin(), addresses.end(), rawAddress);
              return it != addresses.end(); // 返回it是否被找到
          }
          
          • 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
        • 唯一需要解释的一点就是 isOnTheHeap 中的以下语句:
          const void *rawAddress = dynamic_cast<const void*>(this);
          // 这里利用了dynamic_cast的一个特性:
              // 它返回的指针指向原生指针的内存起始处,从而解决了策略3的多继承对象内存不唯一问题
          
          • 1
          • 2
          • 3
        • 任何类如果需要判断 delete 是否安全,只需要继承 HeapTracked 即可。
        • 该方法的缺点是它不能适用于内建类型,因为 int 和 char 等类型不继承自任何东西。
    • 禁止对象产生于堆中
      • 首先回顾一下,对象的存在形式有三种可能:
        • 对象被直接实例化;对象被实例化为 derived class objects 内的 base class 成分;对象被内嵌与其他对象之中。【示例、继承、内含】
      • 要阻止对象直接实例化与 heap 之中,只要将 operator new 或 operator delete 一同设为 private 即可。
      • 如果 derived class 声明自己的 operator new 和 operator delete(或涉及到内含的情况),对象仍然可能位于 heap 内;这时候又回到第二种讨论:没有一个有效办法可以判断一个对象是否位于 heap 内。

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

    • 智能指针,指的是一种类指针对象。【通过封装,使得智能指针和普通指针的使用保持一致】
    • 智能指针的构造、赋值、析构不允许对象的共享,只进行转移。【实现上,在调用 copy constructor 和 assignment 时转移对象所有权,这样在调用 destructor 时就可以直接 delete 智能指针内含的内置指针】
      template<class T>
      class auto_ptr {
      public:
          ...
          auto_ptr(auto_ptr<T>& rhs); 
          auto_ptr<T>&  operator=(auto_ptr<T>& rhs);
          ~auto_ptr(); 
          ...
      private:
                  T *pointee;// 真实的指针
      };
      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;
      }
      template<class T>
      SmartPtr<T>::~SmartPtr()
      {
          if (*this owns *pointee) {// 只有还拥有该指针,才delete,不然不用delete
              delete pointee;
          }
      }
      
      • 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
      • 要注意的另外一点是:以 by-value 的方式在函数间传递智能指针往往是一个非常糟糕的主意。【因为 auto_ptr 的 copy constructor 被调用时,对象所有权便转移了】
        void printTreeNode(ostream& s, auto_ptr<TreeNode> p) // 这里的 p 是传值
        { s << *p; }
        
        int main(){
            auto_ptr<TreeNode> ptn(new TreeNode);
            ...
            printTreeNode(cout, ptn); //通过传值方式传递auto_ptr
            ...
        }
        // 在传参的时候,ptn初始化了p,已经调用了拷贝构造,
            // ptn原本所占有的资源转移到了p中。
        // 当p离开printTreeNode函数的时候,就会调用析构函数去delete了p所占用资源;
            // 程序员的本意只是打印,但经过函数调用的时候,
            // 突然把资源给删除了,这就是为什么不要用by-value。
        
        // pass by reference to const才是正道,修改如下:
        void printTreeNode(ostream& s,const auto_ptr<TreeNode>& p)
        { s << *p; }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
    • Dereference Operators(解引操作符)的实现:【即 operator* 和 operator-> 的实现】
      • operator* 返回所指对象的引用。(如果返回对象,可能会产生由于 SmartPtr 指向的是 T 的派生类对象,而非 T 类对象,而造成切割问题)
        • 如果程序采用了 lazy fetching 策略,就有可能需要为 pointers 变换出一个新对象。
      • operator-> 和 operator* 类似,只是 operator-> 返回指针。
    • 判断 Smart Pointers 是否为空
      • 直接的策略:【定义隐式转换操作符 operator void*,即将智能指针转成 void*】
        // 下述操作就变得允许了:
        if (ptn == 0) ... // 正确
        if (ptn) ... // 正确
        if (!ptn) ... //正确
        
        // 但隐式转换操作符很容易被滥用,它使得不同类型的指针可以相比较:
        SmartPtr<Apple> pa;
        SmartPtr<Orange> po;
        // ...
        if (pa == po) ...//可以通过编译
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
      • 另一种策略:【类内重载 operator!,只有内部指针为空时才返回真】
        SmartPtr<TreeNode> ptn;
        // ...
        if (!ptn) { // 只能用operator !
            // ...
        }
        else {
            // ... 
        }
        // 下述操作被禁止:
        if (ptn == 0) ... 
        if (ptn) ...
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
    • 将 Smart Pointers 转换为 Dumb Pointers
      • 最直观且最易想到的方法,自然是隐式转换操作符:
        template<class T>
        class DBPtr {
        public:
            ...
            operator T*() { return pointee; }
            ...
        };
        // 但如前面强调的那样:隐式转换操作符很容易被滥用
            // 滥用场景1:
        class Tuple{...};
        void processTuple(DBPtr<Tuple>& pt)
        {
            Tuple *rawTuplePtr = pt; // 得到内置指针
            use rawTuplePtr to modify the tuple
        }
            // 滥用场景2:
        DBPtr<Tuple> pt=new Tuple;
        delete pt;//通过了,因为执行了隐式类型转换
            // 但是直接delete dumb pointer必然出现问题
        
        // 不仅会有滥用,更重要的是它还是不能做到提供和内置指针完全一样的行为
            // 因为编译器禁止:连续隐式调用自定义的隐式类型转换
        class TupleAccessors {
        public:
            TupleAccessors(const Tuple *pt); // Tuple到TupleAccessor的转换
            ... 
        };
        TupleAccessors merge(const TupleAccessor& ta1,const TupleAccessors& ta2);
        DBPtr<Tuple> pt1, pt2;
        ...
        merge(pt1,pt2);//调用会出错
        // 调用出错的原因也很简单:
            // DBPtr到Tuple*再到TupleAccessor的转换就可以匹配merge的参数
            // 但是编译器禁止这么做
        
        • 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
      • 解决方法:【使用普通成员函数进行显式转换,以代替隐式转换操作符】
        class DBPtr {
        public:
            ...
            T* toPrimary() { return pointee; }
            ...
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    • Smart Pointers 和与继承有关的类型转换:【使用非虚模板成员函数,不要定义隐式类型转换符】
      • 两个类之间有继承关系,但以这两个类为参数具现化的类模板却没有继承关系。
        • 因为智能指针是类模板,模板参数 T 不同,实例就不同。【智能指针的包装会屏蔽内置指针的继承关系】
        • 当指针经过封装成为智能指针之后,无法实现原来 dumb pointer 所拥有的多态性质
      • 最直接的解决方法:【为每一个智能指针类定义一个隐式类型转换操作符】
        • 所带来的问题也很清楚:必须为每一个智能指针实例定义转换操作符,这与模板的初衷背道而驰;另外,类的继承层次可能很庞大,采用以上方式,继承层次的最底层类的负担将会非常大(必须为对象直接或间接继承的每一个基类提供隐式类型转换操作符)。【治标不治本】
      • 另一种解决方法是:将 non-virtual member function 声明为 templates,利用它来产生指针的转换函数。
        template<class T> 
        class SmartPtr { 
        public:
            SmartPtr(T* realPtr = 0);
            T* operator->() const;
            T& operator*() const;
        
            template<class newType> // 模板成员函数
            operator SmartPtr<newType>() 
            {
                return SmartPtr<newType>(pointee);
            }
            ...
        };
        // 本模板将智能指针之间的隐式类型转换交由底层内置指针来完成
        // 如果底层指针能够转换,那么包装后的智能指针也能够进行转换
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 唯一的缺点是:它是通过指针之间的隐式类型转换来实现指针的多态,也就是说,它实际上并不能区分对象之间的继承层次。【明明是不同层次,但智能指针的转换可能会产生二义性,具体看书中例子】
    • Smart Pointers 与 const
      • 对于普通指针,可以有 const 指针指针 const 之分。
      • 对于智能指针,只有一个地方可以放置 const:【cosnt 只能施行于指针之上,而不能施行于指针所指对象之上】
        const SmartPtr<CD> p=&goodCD;
        
        • 1
      • 也有其他方法去模拟内置指针的行为:
        SmartPtr<CD> p; // non-const 对象, non-const 指针
        SmartPtr<const CD> p; // const 对象,non-const 指针
        const SmartPtr<CD> p = &goodCD; // non-const 对象,const 指针
        const SmartPtr<const CD> p = &goodCD; // const 对象,const 指针
        
        • 1
        • 2
        • 3
        • 4
      • 这种方法的缺陷在于:经由模板包装之后,有继承关系的两个类完全没有关系一样;经由智能指针模板包装后的 const 和 non-const 对象完全不同。
        SmartPtr<CD> pCD = new CD("Famous Movie Themes");
        SmartPtr<const CD> pConstCD = pCD;
        // 以上代码无法通过编译
        
        • 1
        • 2
        • 3
      • 仍然可以用前面提及的隐式类型转换技术来解决,但在这里有所区别的是:const 与 non-const 的转换是单向的;const 转向 non-const 不成问题,但 non-const 转向 const 之后未必可进行下去。
      • 所以这里可以利用这种性质,令每一个 smart pointer-to-T-class public 继承一个对应的 smart pointer-to-const-T class。
        template<class T> // 指向const 对象的
        class SmartPtrToConst {
        protected:
            union {
                const T* constPointee; // 提供给SmartPtrToConst 访问
                T* pointee; // 提供给SmartPtr 访问
            };
        };
        
        template<class T> 
        class SmartPtr: public SmartPtrToConst<T> {
        public:
            template<class constType>
            operator SmartPtrToConst<constType>();
            ... //没有额外数据成员
        };
        
        // SmartPtrToConst使用了union
        	// 这样constPointee和pointee共享同一块内存
        // SmartPtrToConst使用constPointee;
        // SmartPtr使用pointee
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21

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

    • reference counting 使得多个等值对象可以共享同一实值。

    • 以下讨论以自实现的 String 为例,先给出实现代码:【基于引用计数实现的 String 类】

      #include
      #include
      using namespace std;
      
      class String{
      public:
          String(const char* initValue = nullptr);//构造函数
          String(const String& rhs);//拷贝构造函数
          ~String();//析构函数
          String& operator=(const String& rhs);//赋值运算符
          const char& operator[](int index) const;//重载[]运算符,针对const Strings
          char& operator[](int index);//重载[]运算符,针对non-const Strings
      
          String operator+(const String& rhs);//重载+运算符
          String& operator+=(const String& rhs);//重载+=运算符
          bool operator==(const String& rhs);//重载==运算符
          int getLength();//获取长度
          friend istream& operator>>(istream& is, const String& str);//重载>>运算符
          friend ostream& operator<<(ostream& os, const String& str);//重载<<运算符
          int getRefCount();//获取引用对象的个数
      private:
          struct StringValue{
              int refCount;//引用计数
              char* data;
              StringValue(const char* initValue);//构造函数
              ~StringValue();//析构函数
          };
          StringValue* value;
      };
      //StringValue类的构造函数
      String::StringValue::StringValue(const char* initValue):refCount(1){
          if (initValue == nullptr){
              data = new char[1];
              data[0] = '\0';
          }
          else{
              data = new char[strlen(initValue) + 1];
              strcpy(data, initValue);
          }
      }
      //StringValue类的析构函数
      String::StringValue::~StringValue(){
          delete[] data;
          data = nullptr;
      }
      //String类的构造函数
      String::String(const char* initValue):value(new StringValue(initValue))
      {}
      //String类的拷贝构造函数
      String::String(const String& rhs) : value(rhs.value){
          ++value->refCount;//引用计数加1!!!
      }
      //String类的析构函数
      String::~String(){
          if (--value->refCount == 0){//析构时引用计数减1,当变为0时,没有指针指向该内存,销毁
              delete value;
          }
      }
      //String类的赋值运算符
      String& String::operator=(const String& rhs){
          if (value == rhs.value) //自赋值
              return *this;
          //赋值时左操作数引用计数减1,当变为0时,没有指针指向该内存,销毁
          if (--value->refCount == 0)
              delete value;
          //不必开辟新内存空间,只要让指针指向同一块内存,并把该内存块的引用计数加1
          value = rhs.value;
          ++value->refCount;
          return *this;
      }
      //重载[]运算符,针对const Strings
      const char& String::operator[](int index) const{
          if(index<strlen(value->data))
              return value->data[index];
      }
      //重载[]运算符,针对non-const Strings
      char& String::operator[](int index){
          if (value->refCount>1)
          {//如果本对象和其他String对象共享同一实值,
          //就分割(复制)出另一个副本供本对象自己使用
              --value->refCount;
              value = new StringValue(value->data);
          }
          if (index<strlen(value->data))
              return value->data[index];
      }
      //String类的重载+运算符
      String String::operator+(const String& rhs){
          return String(*this) += rhs;
      }
      //String类的重载+=运算符
      String& String::operator+=(const String& rhs){
          //左操作数引用计数减1,当变为0时,没有指针指向该内存,销毁
          if (--value->refCount == 0)
              delete value;
          //右操作数为空
          if (rhs.value->data == nullptr){
              value = new StringValue(value->data);
              return *this;
          }
          //左操作数为空
          if (this->value->data == nullptr){
              value = new StringValue(rhs.value->data);
              return *this;
          }
          //都不空
          char* pTemp = new char[strlen(this->value->data) + strlen(rhs.value->data) + 1];
          strcpy(pTemp, this->value->data);
          strcat(pTemp, rhs.value->data);
          value=new StringValue(pTemp);
          return *this;
      }
      //重载==运算符
      bool String::operator==(const String& rhs){
          return strcmp(this->value->data, rhs.value->data) == 0 ? true : false;
      }
      //获取长度
      int String::getLength(){
          return strlen(this->value->data);
      }
      //重载>>运算符
      istream& operator>>(istream& is, const String& str){
          is >> str.value->data;
          return is;
      }
      //重载<<运算符
      ostream& operator<<(ostream& os, const String& str){
          os << str.value->data;
          return os;
      }
      //获取引用对象的个数
      int String::getRefCount(){
          return value->refCount;
      }
      
      • 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
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
    • Reference Counting(引用计数)的实现:

      • 可以看到,在基础设计上,String 内嵌一个结构体 StringValueStringValue 主要用于存储引用计数和字符串值。【使得引用计数和字符串值相关联】
      • StringValue 只对 String 类可见,而对客户不可见,接口由 String 定义并提供给客户。
      • String有参构造函数只能以初值来构造:【但这样的实现导致分开构造但拥有相同初值的 String 对象,并不共享同一个数据结构
        // String的有参构造函数:
        String::String(const char *initValue): value(new StringValue(initValue)){}
        
        String s1("More Effective C++");
        String s2("More Effective C++");
        // 上述的代码中,s1和s2的值相同,但是却并不共享同一块内存,二十各自拥有独立内存
        // 解决方法在书中并没有讨论,但提供了思路:
        	// 令String统计和追踪现有的StringValue对象,只有面对真正独一无二的字符串时才产生新的StringValue对象
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
      • String拷贝构造函数就利用了 StringValue 的引用计数,这样便可共享内存:【要注意的是 String 中带的成员变量只有一个 StringValue 的指针,拷贝 String 就相当于拷贝一个指针,然后把指针指向的引用计数加一即可】
        String::String(const String& rhs): value(rhs.value){ ++value->refCount; }
        
        • 1
      • String析构函数负责在引用计数为零的时候撤销内存:【先判断引用计数,如果不为零,就什么也不做】
        String::~String()
        {
            if (--value->refCount == 0) 
                delete value;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • String赋值操作符如上面总体代码所示,要注意的只是处理自我赋值的情况。
    • Copy-on-Write(写时才复制):

      • operator[] 的 const 版本:【const 版本代表该函数是只读动作,所以返回指定位置的字符即可】
        const char& String::operator[](int index) const{ return value->data[index]; }
        
        • 1
      • operator[] 的 non-const 版本:【其他可能写入的操作也要参考这种实现,即缓式评估的实现】【non-const 版本面临着写入的可能,对当前 String 的修改不应影响到共享内存的其他 String 对象,所以要先为当前 String 分配独立内存,并将原值进行拷贝
        char& String::operator[](int index)
        {
            if (value->refCount > 1) {// 其实就是缓式评估的一个应用
                --value->refCount; 
                value =new StringValue(value->data); 
            }
            return value->data[index];
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
    • Pointers,References 以及 Copy-on-Write。

      • 前面对 operator[] 的重载解决了可能的写操作篡改共享内存的问题,但是却无法阻止外部指针或引用对共享内存的篡改:【有些 String 实现选择忽略此问题,或者是在文档中提示不要这样做】
        String s1 = "Hello";
        char *p = &s1[1];// 此时p握有对'e'的控制权
        String s2 = s1;
        
        // 接下来更改p的值,s2和s1都会受影响:
        *p = 'x';
        // s1和s2内部无法获知p的操作,因为它们之间并没有联系
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 解决方法是为每一个 StringValue 对象加多一个标志变量,在最初标志设置为 true(表明可以被共享);一旦 non-const operator[] 被调用就将该变量设为 false,并可能永远不许再更改。
        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;
        }
        
        // 其他返回引用的member function(对于String只有operator[])都涉及到对flag的修改
        // 其他可能需要共享内存的member function都涉及到对flag的检测
        String::String(const String& rhs)
        {
            if (rhs.value->shareable) {// 在使用共享内存之前,先测试内存是否允许被共享
                value = rhs.value;
                ++value->refCount;
            }
            else {
                value = new StringValue(rhs.value->data);
            }
        }
        
        • 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
      • 条款 30 的 proxy class 技术可以将 operator[] 的读和写用途加以区分,从而降低需被标记为不可共享之 StringValue 对象的个数。
    • 一个 Reference-Counting(引用计数)基类。

      • 考虑把引用计数抽象为一个类,任何需要 reference-counting 功能的 class 只要使用这个类即可。
      • 基类的实现如下:
        // 定义如下:
        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::RCObject(): refCount(0), shareable(true) {}
            // 拷贝构造函数:
                // 要注意,参数没有名字,也就是说参数没有作用,即拷贝构造函数只是形式上的
                // 至于refCount初始化为0,只是因为要求对象创建者自行将refCount设为1
        RCObject::RCObject(const RCObject&):refCount(0),shareable(true) {}
            // 赋值操作符:
                // 要注意的地方也是一样的,参数没有名字
                // 它什么也不做,仅仅只是返回*this
                // 因为一个RCObject本身就不应该调用赋值操作符,例如StringValue本身就不会调用赋值
                // 即使写出 sv1 = sv2; 这样的语句(其中sv1和sv2是StringValue),指向sv1和sv2的对象数目实际上并未改变
        RCObject& RCObject::operator=(const RCObject&)
        { return *this; }
        RCObject::~RCObject() {} // virtual dtors must always
        void RCObject::addReference() { ++refCount; }
            // removeReference
                // 它还承担了析构函数的作用:
                // 在refCount=1的时候delete销毁对象,从这里可以看出RCObject必须被产生于heap中
        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; }
        
        • 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
      • 基类的使用如下:
        class String {
        private:
                // StringValue继承了RCObject的接口并供String使用,所以StringValue也必须构造在heap中
            struct StringValue: public RCObject {// 直接继承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;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
    • 自动操作 Reference Count(引用计数)。

      • RCObject 提供了一定程度的代码复用功能,但还远远不够。
        • 因为 String 类仍然需要手动调用 RCObject 的成员函数来对引用计数进行更改。
      • 解决方法就是在 StringStringValue 之间加多一层:【加一层智能指针类对引用计数进行管理】
        // 管理引用计数的智能指针类
        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(); // 将构造函数中的重复操作提取成一个函数
        };
        
        // 构造函数如下:
        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); // 这里调用了T的拷贝构造函数
            } 
            pointee->addReference();//引用计数的更改负担转移到这里
        }
        /*
        // 为了防止编译器为StringValue合成的拷贝构造函数执行浅复制
        // 需要为StringValue定义执行深度复制的拷贝构造函数【前面代码中已有体现】
        String::StringValue::StringValue(const StringValue& rhs)// 注意这里讨论的是StringValue的拷贝构造
        {
            data = new char[strlen(rhs.data) + 1];
            strcpy(data, rhs.data);
        }
        */
        
        // 由于多态性的存在,尽管pointee是T*类型,但它实际可能指向T类型的派生类
        	// 在此情况下new调用的却是T的拷贝构造函数
        	// 要防止这种现象,可以使用virtual copy constructor(见条款25),这里不再讨论
        
        // RCPrt的其余实现如下:
        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;
        }
        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; }
        
        • 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
    • 将前面所有的努力进行汇总。

      • StringStringValueRCObject 的关系如图所示:
        关系图

      • 各个类的实现如下:

        //用于产生智能指针的类模板,T必须继承自RCObject
        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();
        };
        //抽象基类用于引用计数
        class RCObject { 
            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;
        };
        //应用性class
        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;
        };
        
        // 具体实现如下:
        
            // RCObject的实现
        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; }
        
            // RCPtr的实现
        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>
        RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs)
        {
            if (pointee != rhs.pointee) {
                if (pointee) pointee->removeReference();
                pointee = rhs.pointee;
                init();
            }
            return *this;
        }
        template<class T>
        T* RCPtr<T>::operator->() const { return pointee; }
        template<class T>
        T& RCPtr<T>::operator*() const { return *pointee; }
        
            // StringValue的实现
        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::String(const char *initValue): value(new StringValue(initValue)) {}
        const char& String::operator[](int index) const
        { return value->data[index]; }
        char& String::operator[](int index)
        {
            //String类唯一需要接触底层成员的负担
            if (value->isShared()) { value = new StringValue(value->data); }             
            value->markUnshareable(); return value->data[index]; 
        }
        // 可以看出String的实现异常简单,因为所有的引用计数任务全部交由其他可移植性类实现
        
        • 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
        • 103
        • 104
        • 105
        • 106
        • 107
        • 108
        • 109
        • 110
        • 111
        • 112
        • 113
        • 114
        • 115
        • 116
        • 117
        • 118
        • 119
        • 120
        • 121
        • 122
        • 123
        • 124
        • 125
    • 将 Reference Counting 加到既有的 Classes 身上。

      • 有了以上设计,就可以实现任何需要引用计数功能的类。【只要继承 RCObject,并作为已有的 RCPtr 模板的类型参数即可】

        • 但程序库中的类却无法更改,假设程序库中存在一个名为 Widget 的类,我们无法修改它;但只要采取之前所用的中间加一层的方法,这种目标仍可以达成。
      • 首先,考虑第一种情况,即 Widget 是我们能修改的;使用方法就是直接令 Widget 继承 RCObject,相当于充当了 StringValue 的角色,具体如下图所示:
        情况一

      • 接着,考虑第二种情况,即 Widget 来自程序库,我们无法修改;采用中间加一层的方法,增加一个新的 CountHolder 类(继承自 RCObject 并持有 Widget 指针);把 RCPtr 类模板用具有相同功能但内部定义了 CountHolderRCIPtr 取代(PCIPtr 的 I 是 indirect 间接的意思),具体如下图所示:
        情况二

        • RCIPtrCountHolder 的实现如下:
          template<class T>
          class RCIPtr {
          public:
              RCIPtr(T* realPtr = 0);
              RCIPtr(const RCIPtr& rhs);
              ~RCIPtr();
              RCIPtr& operator=(const RCIPtr& rhs);
              const T* operator->() const; 
              T* operator->(); 
              const T& operator*() const; 
              T& operator*(); 
          private:
                  // CountHolder只对RCIPter可见,因此设为private
              struct CountHolder: public RCObject {
                  ~CountHolder() { delete pointee; }
                  T *pointee;
              };
              CountHolder *counter;
              void init();
              void makeCopy(); 
          };
          template<class T>
          void RCIPtr<T>::init()
          {
              if (counter->isShareable() == false) {
                  T *oldValue = counter->pointee;
                  counter = new CountHolder;
                  counter->pointee = new T(*oldValue);
              }
              counter->addReference();
          }
          template<class T>
          RCIPtr<T>::RCIPtr(T* realPtr): counter(new CountHolder)
          {
              counter->pointee = realPtr;
              init();
          }
          template<class T>
          RCIPtr<T>::RCIPtr(const RCIPtr& rhs): counter(rhs.counter)
          { init(); }
          template<class T>
          RCIPtr<T>::~RCIPtr()
          { counter->removeReference(); }
          template<class T>
          RCIPtr<T>& RCIPtr<T>::operator=(const RCIPtr& rhs)
          {
              if (counter != rhs.counter) {
                  counter->removeReference();
                  counter = rhs.counter;
                  init();
              }
              return *this;
          }
          template<class T> 
          void RCIPtr<T>::makeCopy()
          { 
              if (counter->isShared()) {
                  T *oldValue = counter->pointee;
                  counter->removeReference();
                  counter = new CountHolder;
                  counter->pointee = new T(*oldValue);
                  counter->addReference();
              }
          }
          template<class T>
          const T* RCIPtr<T>::operator->() const // const版本,只读
          { return counter->pointee; }
          template<class T> 
          T* RCIPtr<T>::operator->() // non-const版本,即copy-on-write的实现
          { 
              makeCopy();
              return counter->pointee; 
          }
          template<class T>
          const T& RCIPtr<T>::operator*() const // const版本,只读
          { return *(counter->pointee); }
          template<class T>
          T& RCIPtr<T>::operator*() // non-const版本,即copy-on-write的实现
          { 
              makeCopy(); 
              return *(counter->pointee); 
          }
          
          • 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
        • 有引用计数功能的 RCWidget 只要通过底层的 RCIPtr 调用对应的 Widget 函数即可:
          // Widget的接口如下:
          class Widget {
          public:
              Widget(int size);
              Widget(const Widget& rhs);
              ~Widget();
              Widget& operator=(const Widget& rhs);
              void doThis();
              int showThat() const;
              // ...
          };
          
          // RCWidget定义如下:
          class RCWidget {
          public:
              RCWidget(int size): value(new Widget(size)) {}
              void doThis() { value->doThis(); }
              int showThat() const { return value->showThat(); }
          private:
              RCIPtr<Widget> value;
          };
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
    • 最后的总结:

      • 引用计数也是需要成本的,只有在下面的情况下才会发挥它的优化功能:
        1. 相对多的对象,共享相对少的内存。
        2. 对象实值的产生和销毁成本太高,或使用了太多内存。
      • 如果引用计数使用不当,反而会降低程序效率。

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

    • 所谓代理类(proxy class),指的是它的每一个对象都是为了其他对象而存在的,就像是其他对象的代理人一般。
      • 某些情况下用代理类取代某些内置类型可以实现独特的功能,因为可以为代理类定义成员函数而但却无法对内置类型定义操作。
      • 条款 5 就展示了一个使用代理类阻止隐式类型转换的例子。
    • 实现二维数组:
      • C++ 没有提供分配动态二维数组的语法。首先考虑为二维数组定义一个 class template:
        template<class T>
        class Array2D {
        public:
            Array2D(int dim1, int dim2);
            ...
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
      • 重点就是提供使用 [][] 访问元素的操作,然而 [][] 并不是一个操作符,C++ 也就不允许重载一个 operator[][]。解决方法就是采用代理类:
        template<class T>
        class Array2D {
        public:
            //代理类
            class Array1D {
            public:
                T& operator[](int index);
                const T& operator[](int index) const;
            ...
            };
            Array1D operator[](int index);
            const Array1D operator[](int index) const;
            ...
        };
        
        // 如此一来,以下操作便可实现:
        Array2D<float> data(10, 20);
        ...
        cout << data[3][6];
        // data[3][6]实际上进行了两次函数调用:
            // 第一次调用Array2D的operator[],返回Array1D对象
            // 第二次调用Array1D的operator[],返回指定元素
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
    • 区分 operator[] 的读写操作。
      • 在条款 29 中,实现non-const的时候还未进行本章节的讨论,所以条款 29 中 non-const operator[] 的实现是直接开辟一块新内存并复制数据结构到新内存。【没有区分读写操作】
      • 使用 proxy class 便可以做到区分 non-const operator[] 用于读还是写操作:
        class String {
        public:
            //代理类用于区分operator[]的读写操作
            class CharProxy { // proxies for string chars
            public:
                CharProxy(String& str, int index); // creation
                CharProxy& operator=(const CharProxy& rhs); // 左值运用,意味着等号左边是CharProxy
                                // 如果左边是CharProxy,说明是作为左值存在的,此时便调用上面这个版本
                CharProxy& operator=(char c); 
                operator char() const; // 右值运用,就直接转成char,然后读出去了
            private:
                String& theString; //用于操作String,并在适当时机开辟新内存并复制
                int charIndex;
            };
            const CharProxy operator[](int index) const; // for const Strings
            CharProxy operator[](int index); // for non-const Strings
            ...
            friend class CharProxy;
        private:
            RCPtr<StringValue> value;//见条款29
        };
        
        // 对String调用operator[]将返回CharProxy对象
            // CharProxy通过重载oeprator char模拟char类型的行为
        // 当对CharProxy使用operator=时,便可得知是对CharProxy进行写操作
        // 具体实现如下:
        String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {}
        String::CharProxy::operator char() const
        {
        return theString.value->data[charIndex];
        }
        String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)// 左值,就是写操作,需要开辟内存并构造
        {
            if (theString.value->isShared()) {
                theString.value = new StringValue(theString.value->data);
            }
            theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
            return *this;
        }
        String::CharProxy& String::CharProxy::operator=(char c)
        {
            if (theString.value->isShared()) {
                theString.value = new StringValue(theString.value->data);
            }
            theString.value->data[charIndex] = c;
            return *this;
        }
        // 以上函数的代码部分有重复,可考虑将重复部分提取成一个函数
        
        // String的operator[]相当简单
        const String::CharProxy String::operator[](int index) const
        {
            return CharProxy(const_cast<String&>(*this), index);
        }
        String::CharProxy String::operator[](int index)// non-const版本,直接返回CharProxy对象
        {
            return CharProxy(*this, index);
        }
        
        • 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
    • 局限性:
      • 就像智能指针永远无法完全取代内置指针一样,proxy class 也永远无法模仿内置类型的所有特点,为了模仿内置类型的其他特点,它还要打许多补丁:
        1. 对 proxy class 取址。
          • 条款 29 通过为 StringValue 类添加可共享标志来表示对象是否可被共享以防止外部指针的篡改,其中涉及到对 operator[] 返回值进行取址操作。
          • 所以 CharProxy 也需要对 operator& 进行重载:
            // 定义如下:
            class String {
            public:
                class CharProxy {
                public:
                    ...
                    char * operator&();
                    const char * operator&() const;
                    ...
                };
                ...
            };
            
            // 具体实现如下:
                // const版本operator&实现比较直观:
            const char * String::CharProxy::operator&() const
            {
                return &(theString.value->data[charIndex]);
            }
                // non-const版本的operator&要更复杂些:
            char * String::CharProxy::operator&()
            {
                //如果正在使用共享内存,就开辟新内存并复制数据结构
                if (theString.value->isShared()) {
                    theString.value = new StringValue(theString.value->data);
                }
                //由于有外部指针指向它,因此有被篡改风险,禁止使用共享内存
                theString.value->markUnshareable();
                return &(theString.value->data[charIndex]);
            }
            
            • 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
        2. operator[] 无法和其他 operator 复合使用。
          • 目前只看到了 operator[] 和 operator= 的交互,还未看到和其他操作符的复合使用:【要用到哪个操作符,就得重载】
            template<class T>
            class Array{
            public:
                class Proxy{...};// 假设根据这个代理类重载了operator[]
            }
            // 下述代码仍然错误:
            Array[5] += 1;
            Array[5] ++;
            // Array[]产生了一个代理类Proxy,但这个代理类无法作为operator +=的左手端
            // 因而只能一一重载operator++、operator+=
            
            • 1
            • 2
            • 3
            • 4
            • 5
            • 6
            • 7
            • 8
            • 9
            • 10
        3. 通过 proxy cobjects 调用真实对象的 member function。
          • 这个问题与问题二比较相似:
            // 假设有个Rational类,里面有成员函数A、成员函数B
            // 我们再使用前面问题二中的Array模板
            Array<Rational> array;
            cout << array[1].A();// 错误!
            int tmp = array[2].B();// 错误!
            // 错误原因很简单:
                // array[]返回的是代理类,不是真正的Rational,所以无法调用成员函数
            // 解决方法也很麻烦:需要将适用于真实对象的每一个函数加以重载
            
            • 1
            • 2
            • 3
            • 4
            • 5
            • 6
            • 7
            • 8
        4. 将 proxy class 传递给接受 references to non-const objects 的函数。
          • 假设有一个 swap 函数用于对象两个 char 的内容:
            void swap(char& a, char& b);// 注意传参是引用,但代理类只能转成char
                // 即使能转成char&,也有问题;因为临时对象转成non-const reference参数也是不允许的
            
            // 无法将CharProxy做参数传递给swap
                // 因为swap的参数是char&
                // 尽管CharProxy可以转换到char,但由于抓换后的char是临时对象,仍然无法绑定到char&
            
            • 1
            • 2
            • 3
            • 4
            • 5
            • 6
        5. 隐式类型转换。
          • proxy class 要具有和被代理类型相同的行为,通常的做法是重载隐式转换操作符。
          • 但用户定制的隐式类型转换不能连续实行两次,Proxy 类可能因为这个特点而阻止了用户需要的隐式类型转换。【如果不用代理,只是一次隐式类型转换,但因为用了代理类,相当于增加了中间层,所以有时会需要两次隐式类型转换】

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

    • 该问题描述如下:
      • 首先有这么一个继承体系:
        class GameObject { ... };// 抽象基类,游戏物体
        class SpaceShip: public GameObject { ... };// 宇宙飞船
        class SpaceStation: public GameObject { ... };// 空间站
        class Asteroid: public GameObject { ... };// 小行星
        
        // 底下的三个对象相撞有不同的规则,处理碰撞的函数声明如下:
        void checkForCollision(GameObject& object1,GameObject& object2);
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • checkForCollision 函数的参数可知,要处理两个物体的碰撞,必须知道这两个引用的动态类型。
        • 但是 C++ 支持的虚函数只支持 single-dispatch(即单参数传递)。
        • dispatch 在这里意思是分派,虚函数调用常被称为消息分派(message dispacth);函数调用如果根据两个参数而虚化,就被称为 double dispatch(multiple dispatch同理)。
      • 接下来的讨论就是如何实现 C++ double dispatch。
    • 方法一:虚函数 + RTTI(运行时类型识别)。
      • 最朴素的方法是使用 if-else 语句结合 RTTI:【每个类都带一个类似函数】【注意基类没有该函数】
        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);
                process a SpaceShip-Asteroid collision;
            }
            //处理不明撞击物
            else {
                throw CollisionWithUnknownObject(otherObject);
            }
        }
        
        • 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
      • 缺点也很明显:加入新的类型,继承体系中每一个类的 collide 函数可能都需要添加处理新型碰撞的代码;而且使用 RTTI 实现 double-dispatching,也将根据参数动态类型采取不同行为的负担加在了程序员身上。
    • 方法二:只使用虚函数。【无 RTII】
      • 这种方法的基本思想是通过对虚函数 collide 的重载,将 double dispatch 以两个 single dispatch 实现:【类似于递归思想】
        class SpaceShip; // 前置声明
        class SpaceStation;
        class Asteroid;
        class GameObject {
        public:
            virtual void collide(GameObject& 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(GameObject& otherObject);
            virtual void collide(SpaceShip& otherObject);
            virtual void collide(SpaceStation& otherObject);
            virtual void collide(Asteroid& otherobject);
            ...
        };
        
        // 关于GameObject&的实现如下:
        void SpaceShip::collide(GameObject& otherObject)
        {
            otherObject.collide(*this);// 在这里会交换指针
                // 在该函数内部,*this实际上已经对应该函数的动态类型
        }
        // 其他实现如下:
        void SpaceShip::collide(SpaceShip& otherObject)
        {
            process a SpaceShip-SpaceShip collision;// 实际碰撞操作
        }
        void SpaceShip::collide(SpaceStation& otherObject)
        {
            process a SpaceShip-SpaceStation collision;
        }
        void SpaceShip::collide(Asteroid& otherObject)
        {
            process a SpaceShip-Asteroid collision;
        }
        
        • 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
      • 这种方法不需要使用 RTTI,但却有和 RTTI 一样的缺点:
        • 一旦有新的 class 加入,代码就必须修改。
        • 修改类定义会引起包含这些类定义的文件的重新编译,在很多情况下成本相当大。
    • 方法三:自行仿真虚函数表格。
      • 由编译器使用虚函数表实现动态绑定的策略启发,可以自行仿真一个虚函数表格。
        • 它保存类名和对应碰撞处理函数指针的键值对,并进行类名到碰撞处理函数的映射,达到 double dispatch 的目的。
      • 修改 SpaceShip 的定义如下:
        class GameObject {
        public:
            virtual void collide(GameObject& otherObject) = 0;// 注意,不再重载collide函数,只有一个
            ...
        };
        class SpaceShip: public GameObject {
        public:
            virtual void collide(GameObject& otherObject);// 注意,这里collide函数的参数仍然是GameObject&
            virtual void hitSpaceShip(SpaceShip& otherObject);// 注意,这三行函数画线的地方后面会更改
            virtual void hitSpaceStation(SpaceStation& otherObject);
            virtual void hitAsteroid(Asteroid& otherobject);
            ...
        };
        void SpaceShip::hitSpaceShip(SpaceShip& otherObject)// 这三个函数后面会给出具体实现
        {
            process a SpaceShip-SpaceShip collision;
        }
        void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
        {
            process a SpaceShip-SpaceStation collision;
        }
        void SpaceShip::hitAsteroid(Asteroid& otherObject)
        {
            process a SpaceShip-Asteroid collision;
        }
        
        // 在collide中,我们需要一种方法,将参数otherObject的动态类型映射到某个member function指针,
            // 指针就能指向适当的碰撞处理函数。
        // 这里实现了一个lookup函数,交给lookup一个GameObject,它会返回一个指针,指向必须调用的member function
        class SpaceShip: public GameObject {
        private:
            typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
            //碰撞处理函数指针
            static HitFunctionPtr lookup(const GameObject& whatWeHit);// 声明
            ...
        };
        
        // collide对lookup的使用如下:
        void SpaceShip::collide(GameObject& otherObject)
        {
            HitFunctionPtr hfp =lookup(otherObject);
            if (hfp) { 
                (this->*hfp)(otherObject); // call it
            }
            //处理未知碰撞
            else {
                throw CollisionWithUnknownObject(otherObject);
            }
        }
        
        // 虚函数表和lookup的实现如下:
            // 虚函数表可以用STL内的map容器实现
            // 它还要保证在第一次调用lookup函数时,就已被初始化并在程序结束后被释放
            // 比较好的选择是把它声明为lookup内的static对象
        class SpaceShip: public GameObject {
        private:
            typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
            typedef map<string, HitFunctionPtr> HitMap;// map的first是对象名,即是SpaceShip还是SpaceStation
                                                            // map的second是函数指针
            ...
        };
        SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
        {
            static HitMap collisionMap;
            //查找
            HitMap::iterator mapEntry=collisionMap.find(typeid(whatWeHit).name());
            //未知碰撞
            if (mapEntry == collisionMap.end()) // 没找到,返回0
                return 0;
            return (*mapEntry).second;// 找到,返回函数指针
        }
        
        • 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
      • 接下来考虑将虚函数表格初始化。【就是前面代码中 static 的 collisionMap
        • 错误的示范如下:
          SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
          {
              static HitMap collisionMap;// 应该在初始化的时候直接获得这个map,所以要另写一个初始化HitMap的函数
              collisionMap["SpaceShip"] = &hitSpaceShip;
              collisionMap["SpaceStation"] = &hitSpaceStation;
              collisionMap["Asteroid"] = &hitAsteroid;
              ...
          }
          // 会造成每次调用lookup时
              // 都将hitSpaceShip,hitSpaceStation,hitAsteriod插入collisionMap内
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
        • 正确的思路如下:
          class SpaceShip: public GameObject {
          private:
              static HitMap initializeCollisionMap();
              ...
          };
          SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
          {
                  // 区分下面两行的写法,注释掉的代码要付出构造和析构临时HitMap对象的成本
              // static HitMap collisionMap = initializeCollisionMap();
              static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
                  ...
          }
          
          // initializeCollisionMap的实现如下:
          SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
          {
              HitMap *phm = new HitMap;
              //正是为了以下操作,之前collide才放弃重载以使得hitSpaceShip,hitSpaceStation,hitAsteriod可以有相同参数
              (*phm)["SpaceShip"] = &hitSpaceShip;
              (*phm)["SpaceStation"] = &hitSpaceStation;
              (*phm)["Asteroid"] = &hitAsteroid;
              return phm;
          }
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
      • 但依照前面的实现,initializeCollisionMap 无法通过编译,原因是:
        • HitMap 被声明为用来存放成员函数指针,这些指针都需要有相同的类型的参数。
        • 一种糟糕的解决方法是:在 initializeCollisionMap 中每个语句下面用 reinterpret_cast。虽然这种做法通过了编译,但是通过欺骗编译器的做法达成的效果;在菱形继承的场景下,这种欺骗行为可能导致编译器传递了错误的地址。
        • 更好的解决方法是把前面的三个函数(hitSpaceShiphitSpaceStationhitAsteroid)的参数进行更改:
          void SpaceShip::hitSpaceShip(GameObject& spaceShip)
          {
              SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip);// 内部强制转换
              process a SpaceShip-SpaceShip collision;
          }
          void SpaceShip::hitSpaceStation(GameObject& spaceStation)
          {
              SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation);
              process a SpaceShip-SpaceStation collision;
          }
          void SpaceShip::hitAsteroid(GameObject& asteroid)
          {
              Asteroid& theAsteroid =dynamic_cast<Asteroid&>(asteroid);
              process a SpaceShip-Asteroid collision;
          }
          
          //如前面所说,这三个函数的参数都统一成了GameObject&:
                  ...
                  virtual void hitSpaceShip(GameObject& otherObject);
              virtual void hitSpaceStation(GameObject& otherObject);
              virtual void hitAsteroid(GameObject& otherobject);
                  ...
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
      • 到目前为止,仿真虚函数表的方法仍然不能解决前面两个策略共同的问题:当有新的 class 加入时,继承体系的每个类都需要添加处理新型碰撞的代码。
        • 这是因为此前的策略都是将处理碰撞的任务交由碰撞的某一方来执行。
          • 仿真虚函数表策略也不例外:每个 class 内含一个仿真的虚函数表,内含的指针也都指向成员函数。
        • 将碰撞处理函数设为 non-member,就可以使得 class 定义式不包含碰撞处理函数。【当需要添加碰撞处理函数时也就不需要修改 class 定义】【将碰撞处理函数从类中分离】
          #include "SpaceShip.h"
          #include "SpaceStation.h"
          #include "Asteroid.h"
          //使用匿名命名空间使得名称只对本单元可见,作用等同于将名称声明为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& asteroid){ asteroidStation(asteroid, spaceStation); }
              ...
              typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
              typedef map< pair<string,string>, HitFunctionPtr > HitMap;
              pair<string,string> makeStringPair(const char *s1,const char *s2);
              HitMap * initializeCollisionMap();
              HitFunctionPtr lookup(const string& class1,
              const string& class2);
          } //命名空间结束
          void processCollision(GameObject& object1,GameObject& object2)
          {
              HitFunctionPtr phf = lookup(typeid(object1).name(),typeid(object2).name());
              if (phf) 
                  phf(object1, object2);
              else 
                  throw UnknownCollision(object1, object2);
          }
          // 和前面的实现差别如下:
          	// HitFunctionPtr是一个typedef,表示指向non-member function的指针
          	// exception class CollisionWithUnknownObject被重命名为UnKnownCollision并使用接受两个GameObject对象的构造函数
          	// lookup必须接获两个GameObject名称并执行double dispatch
          	// 为了使得map的键含有两份信息,需要使用标准库pair类模板
          
          // 具体实现如下:
          namespace {
              pair<string,string> makeStringPair(const char *s1,const char *s2){ return pair<string,string>(s1, s2); }
          }
          namespace {
              HitMap * initializeCollisionMap()
              {
                  HitMap *phm = new HitMap;
                  (*phm)[makeStringPair("SpaceShip","Asteroid")] =&shipAsteroid;
                  (*phm)[makeStringPair("SpaceShip", "SpaceStation")] =&shipStation;
                  ...
                  return phm;
              }
          } 
          namespace {
              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;
              }
          }
          // 因为makeStringPair、initializationCollisionMap和lookup都被声明于一个匿名namespace内
          // 所以它们也必须实现于相同的namespace内,使得链接器能正确将其定义和声明关联
          
          • 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
          • 即使新的 GameObject 被添加,原有的 class 也不需要重新编译。
            • 只需要在 initializeCollisionMap 中增加对应的键值对,并在 processCollision 所在的匿名命名空间中申明一个新的碰撞处理函数即可。
      • 现在,处理这个方法存在的最后一个问题:
        • 如果在调用碰撞处理函数时,发生了 inheritance_based 类型转换,方法就没法有效运作。

        • 我们现在假设继承体系中增添了两个新类:【注意书中这个位置,GameObjectSpaceStaion 都被设置为了抽象基类】【其中 CommercialShipMilitaryShip 的碰撞行为完全相同,即 SpaceStation 的子类行为是一致的】
          增添新类

        • 如果 MilitrayShipAsteroid 碰撞,程序员可能企图通过调用下面语句来解决:

          void shipAsteroid(GameObject& spaceStation,GameObject& asteroid);
          
          • 1
        • 结果并不如意,会有未知碰撞异常抛出。

          • 即使 MilitaryShip 可以视作 SpaceStation,但 lookup 函数查不出来;因为 lookup 是靠字符匹配的方式查找对应函数指针。
        • 在这里,如果又要实现 double dispatching,还要支持 inheritance_based 类型转换,我们只能回头去倚靠那个双虚函数调用机制。

      • 最后的最后,文章中再度对将虚函数表格初始化进行了讨论。【即把 HitMap 类单独设计,让程序员可以随时进行增删查改】
        • 截至目前,整个设计是静态的:虚函数表在程序开始就生成,此后不再发生改变。
          • 但整个设计还有发挥空间:增加对仿真虚函数表做新增、删除、修改动作的功能。
          • 由于对虚函数表可以做多种操作,因此可以考虑定义一个 CollisionMap 类用于管理仿真虚函数表,并提供新增、删除、修改虚函数表的接口。
          • 代码如下:
            // 定义如下:
            class CollisionMap {
            public:
                typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
                void addEntry(const string& type1,const string& type2,HitFunctionPtr collisionFunction,bool symmetric = true);//symmetric用于标记是否产生两个顺序不同的碰撞处理函数,默认为true
                void removeEntry(const string& type1,const string& type2);
                HitFunctionPtr lookup(const string& type1,const string& type2);
                //产生唯一的CollisionMap
                static CollisionMap& theCollisionMap();
            private:
                //构造函数声明为private从而限制CollisionMap只能产生一个
                CollisionMap();
                CollisionMap(const CollisionMap&);
            };
            
            // 对CollisionMap的使用如下:
            void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
            CollisionMap::theCollisionMap().addEntry("SpaceShip","Asteroid",&shipAsteroid);
            void shipStation(GameObject& spaceShip,GameObject& spaceStation);
            CollisionMap::theCollisionMap().addEntry("SpaceShip","SpaceStation",
            &shipStation);
            void asteroidStation(GameObject& asteroid,GameObject& spaceStation);
            CollisionMap::theCollisionMap().addEntry("Asteroid","SpaceStation",&asteroidStation);
            ...
            
            • 1
            • 2
            • 3
            • 4
            • 5
            • 6
            • 7
            • 8
            • 9
            • 10
            • 11
            • 12
            • 13
            • 14
            • 15
            • 16
            • 17
            • 18
            • 19
            • 20
            • 21
            • 22
            • 23
            • 24
        • 设计 CollisionMap 类后,产生的问题有:
          • 确保在碰撞发生之前对应的函数条目就被加入到了 map 之中。
          • 解决方法之一:令 GameObject 的 subclass 的 constructor 进行检查,看看是否在对象产生之际已有适当的 map 条目加入,这需要时间的成本。
          • 解决方法之二:使用一个 RigisterCollisionFunction 类进行管理。
            class RegisterCollisionFunction {
            public:
                RegisterCollisionFunction(const string& type1,const string& type2,CollisionMap::HitFunctionPtr collisionFunction,bool symmetric = true)
                {
                    CollisionMap::theCollisionMap().addEntry(type1, type2,collisionFunction,symmetric);
                }
            };
            // 用户使用RigisterCollisionFunction进行“注册”:
            RegisterCollisionFunction cf1(typeid(SpaceShip).name(), typeid(Asteroid).name(),&shipAsteroid);
            RegisterCollisionFunction cf2(typeid(SpaceShip).name(), typeid(SpaceStation).name(),&shipStation);
            RegisterCollisionFunction cf3(typeid(Asteroid), typeid(SpaceStation).name(),&asteroidStation);
            ...
            int main(int argc, char * argv[])
            {
                ...
            }
            // 这些全局对象在main函数调用之前就产生
                // 因此它们的constructor所注册的条目也会在main被调用之前加入map
            
            // 如果此后有新的derived class和相应的碰撞函数产生:
            class Satellite: public GameObject { ... };
            void satelliteShip(GameObject& satellite,GameObject& spaceShip);
            void satelliteAsteroid(GameObject& satellite,GameObject& asteroid);
            // 函数可以使用类似方法加入到map之中,而不需要改变原有代码:
            RegisterCollisionFunction cf4(typeid(Satellite).name(),typeid(SpaceShip).name(),&satelliteShip);
            RegisterCollisionFunction cf5(typeid(Satellite).name(),typeid(Asteroid).name(),&satelliteAsteroid);
            
            • 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
  • 相关阅读:
    批量删除Docker容器
    vue3项目开发技术点总结
    最好的PDF压缩:ImageGear PDF 25.2.NET
    Kotlin2 进阶
    everything项目测试篇
    【二叉树】寻找重复的子树
    基于Vue前端框架构建BI应用程序
    第五章 创建、查看和编辑文本
    Java agent 使用详解
    docker发布dubbo服务 外部程序访问不到问题处理
  • 原文地址:https://blog.csdn.net/weixin_44705592/article/details/126735261