• c++ 沉思录笔记——句柄(第一部分)


    句柄:第一部分

    代理类可以让我们在一个容器中存储不同类型但相互关联的对象。

    这种方法需要为每一个对象创建一个代理,并要将代理存储在容器中。

    创建代理将会复制所代理的对象,就像复制代理一样。

    句柄类:允许在保持代理的多态行为的同时,还可以避免进行不必要的复制。

    6.1 问题

    对于某些类来说,能够避免复制其对象是很有好处的。

    1)对象会很大,复制起来消耗太大;

    2)可能每个对象代表一种不能轻易被复制的资源,比如文件。

    3)某些其他的数据结构已经存储了对象的地址,把副本的地址插入到那些数据结构中代价会非常大,或者根本不可能。

    4)这个对象代表着位于网络连接另一端的其他对象。

    5)我们可能处于一个多态性的环境中,我们能够知道对象的基类的类型,但是不知道对象本身的类型或者怎样复制这种
    类型的对象。

    需要一种方法,让我们在避免某些缺点(如缺乏安全性)的同时能够获取指针的某些优点,尤其是能够在保持多态性的
    前提下避免复制对象的代价。

    c++的解决办法就是定义一个适当的类。由于这些类的对象通常被绑定到它们所控制的类的对象上,所以这些类常称为
    handle类(handle classes)。因为这些 handle的行为类似指针,所有有时候人们也叫它们只能指针。然后两者还
    是有很大差别,只有在极其有限的情况下才能把两者是做相同。

    6.2 一个简单的类

    class Point {
    public:
        Point() : xval(0), yval(0) { }
        Point(int x, int y) : xval(x), yval(y) { }
        int x() const { return xval; }
        int y() const { return yval; }
        Point& x(int xv) { xval = xv; return *this; }
        Point& y(int yv) { yval = yv; return *this; }
    
    private:
        int xval, yval;
    };
    Point p;
    int x = p.x(); //把p的x坐标复制到x中
    p.x(42); // 将p的x坐标设置为42,这个操作会返回调用对象的引用。
    p.x(42).y(23); // 将p的x坐标设置为42,y坐标设置为23
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    6.3 绑定到句柄

    从Point对象初始化 handle 应该完成那些任务呢?

    Point p;
    
    Handle h(p); // 这应该是什么含义?
    
    • 1
    • 2
    • 3

    浅显的说,我们希望将句柄h直接绑定到对象p上。(绑定:用p初始化h?)

    将handle直接绑定到对象p上,则我们的handle最好与p的内存分配和释放无关。

    handle应该“控制”它所绑定的对象,也就是handle应该创建和销毁对象。

    两种选择:

    1)可以创建自己的Point对象并把它赋给一个handle去进行复制;// Handle h§; // 创建一个p的副本,并将handle绑定到该副本!

    1. 可以把用于创建Point对象的参数传递给Handle。// Handle h0(123, 456); // 创建绑定到新分配的坐标为(123,456)的Point的handle

    因此,我们想要Handle类的构造函数和Point类一样。

    6.4 获取对象

    假设我们有一个绑定到point对象的handle,应该怎么访问这个 point呢?

    要是一个handle在行为上类似一个指针,则可以使用 operator-> 将Handle的所有操作转发给相应的Point操作来执行:

    class Handle {
    public:
        Point* operator->();
        // ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    作用:把所有的Point操作都通过operator->转发了。

    缺点:没有简单的办法禁止一些操作,也没有办法选择地改写一些操作。

    例如,如果我们希望handle能够对对象的分配和回收拥有完全的控制权,那么最好组织用户可以直接获取对象的实际地址。
    但是如果我们handle有operator->操作,则可以使用 c++ Point* addr = h.operator->();从而获得底层Point对象的地址。

    所以,如果想把Point对象的真实地址隐藏起来,就必须避免使用operator->,而且必须明确地选择让我们的Handle类支持哪些Point操作。

    6.5 简单的实现

    开始实现自己的Handle类!

    class Handle {
    public:
        Handle();
        Handle(int,int);
        Handle(const Point&);
        Handle(const Handle&);
        Handle& operator= (const Handle&);
        ~Hanlde();
    public:
        int x() const;
        Handle& x(int);
        int y() const;
        Handle& y(int);
    
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    6.6 引用计数型句柄

    之所以用句柄,原因之一就是避免不必要的对象复制。也就是允许多个句柄绑定到单个对象上。

    通常使用引用计数(use_count)来了解有多少个句柄绑定到同一个对象上,只有这样才能确定应该在合适删除对象。

    1)这个引用计数不是能是句柄的一部分。如果这么干,那么每一个句柄都必须知道跟它一起被绑定到同一个对象的其他所有句柄的位置,
    如此才能去更新其他句柄的引用计数数据。

    2)不能让引用计数成为对象的一部分,因为那样要求我们重写已经存在的对象类。

    因此,我们必须定义一个新的类来容纳一个 引用计数Point类。我们称之为UPoint

    这个类纯粹是为了实现而设计的,因此把它的所有成员设置为 private,并且将我们的Handle类设置为有元。

    我们生成一个 UPoint对象时,其引用计数始终为1,因为该对象的地址将会马上存储在一个Handle对象中。(由于UPoint类的所有成员都是private的,
    所以如果有一个UPoint对象被生成,则必然是其友元类Handle要求的,因此可以肯定,UPoint对象的地址将会马上被Handle对象保存。)

    另一方面,我们希望能够以创建Point类的全部方式创建UPoint对象,所以把Point的构造函数照搬过来:

    class UPoint{
    private: // 所有成员都是private
        friend class Handle;
        Point p_;
        int u_; // 引用计数数据
    
        UPoint() : u(1) { }
        UPoint(int x, int y) : Point(x, y), u_(1) { }
        UPoint(const Point& p) : p_(p), u_(1) { }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    除此之外,我们将通过直接引用UPoint对象的方式操作UPoint对象。(?)

    开始完善Handle类

    class Handle {
    public:
        Handle() : up_(new UPoint) { }
        Handle(int x, int y) : up_(new UPoint(x, y)) { }
        Handle(const Point& p) : up_(new UPoint(p)) { }
        Handle(const Handle& h) : up_(h.up_) { ++up_->u; } // 复制构造函数,只用把引用计数加1,这样原先的句柄和副本都指向相同的UPoint对象。
        Handle& operator= (const Handle& h) {
            // 首先递增右侧句柄指向对象的引用计数
            ++h.up_->u;
            // 然后递减左侧句柄所指向对象的引用计数
            if (--up_->u == 0)
                delete up_;
            up_ = h.up_;
            return *this;
        }
        ~Hanlde() { // 递减引用计数,如果发现计数为0,就删除 UPoint对象
            if (--up->u_ == 0) {
                delete up_;
            }
        }
    public:
        int x() const { return up_->p.x(); }
        Handle& x(int x);
        int y() const { return up_->p.y(); }
        Handle& y(int);
    
    private:
        // 添加的
        UPoint *up_;
    };
    
    • 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

    然而,当我们开始考虑改动性的函数时,事情就变得有趣了。原因是:这里我们必须做出决定,我们的句柄需要值语义还是指针语义?

    6.7 写时复制

    从实现的角度看,我们将Handle类设计成“无需对Point对象进行复制”的形式。
    可关键问题是:是否希望句柄类在用户面前的行为也是这样的。
    例如:

    Handle h(3, 4);
    Handle h2 = h;  // 复制 Handle
    h2.x(5);        // 修改 Point
    int n = h.x();  // 3? or 5?
    
    • 1
    • 2
    • 3
    • 4

    如果Handle采用值语义,那么 n= 3;采用指针语义 n= 5;

    如果采用指针语义,则永远不必复制 UPoint对象。

    Handle& Handle::x(int x0) {
        up_->p.x(x0);
        return *this;
    }
    Handle& Handle::y(int y0) {
        up_->p.y(x0);
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果采用值语义,就必须保证所改动的哪个UPoint对象不能同时被任何其他的Handle所引用。
    这也不难,只要看看引用计数即可。如果是1,则说明Handle是唯一一个使用该UPoint对象的句柄;
    其他情况下,就必须复制另外一个UPoint对象,使其引用计数变成1:

    // 完整代码!!!!!!!!!
    class Point
    {
    public:
        Point()
            : xval(0), yval(0)
        {}
        Point(int x, int y)
            : xval(x), yval(y)
        {}
        int x() const
        { return xval; }
        int y() const
        { return yval; }
        Point &x(int xv)
        {
            xval = xv;
            return *this;
        }
        Point &y(int yv)
        {
            yval = yv;
            return *this;
        }
    
    private:
        int xval, yval;
    };
    
    class UPoint
    {
    private: // 所有成员都是private
        friend class Handle;
        Point p_;
        int u_; // 引用计数数据
    
        UPoint()
            : u_(1)
        {}
        UPoint(int x, int y)
            : p_(x, y), u_(1)
        {}
        explicit UPoint(const Point &p)
            : p_(p), u_(1)
        {}
    };
    
    class Handle
    {
    
    public:
        Handle()
            : up(new UPoint)
        {}
        Handle(int x, int y)
            : up(new UPoint(x, y))
        {}
        explicit Handle(const Point &p)
            : up(new UPoint(p))
        {}
        Handle(const Handle &h)
            : up(h.up)
        { ++up->u_; } // 复制构造函数,只用把引用计数加1,这样原先的句柄和副本都指向相同的UPoint对象。
        Handle &operator=(const Handle &h)
        {
            // 首先递增右侧句柄指向对象的引用计数
            ++h.up->u_;
            // 然后递减左侧句柄所指向对象的引用计数
            if (--up->u_ == 0)
                delete up;
            up = h.up;
            return *this;
        }
        ~Handle()
        { // 递减引用计数,如果发现计数为0,就删除 UPoint对象
            if (--up->u_ == 0) {
                delete up;
            }
        }
    public:
        int x() const
        { return up->p_.x(); }
        int y() const
        { return up->p_.y(); }
    
        Handle &x(int x0)
        {
            if (up->u_ != 1) {
                --up->u_;
                up = new UPoint(up->p_);
            }
            up->p_.x(x0);
            return *this;
        }
        Handle &y(int y0)
        {
            if (up->u_ != 1) {
                --up->u_;
                up = new UPoint(up->p_);
            }
            up->p_.y(y0);
            return *this;
        }
    private:
        // 添加的
        UPoint *up;
    };
    
    • 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

    这一技术通常成为 copy on write(写时复制)。

    优点:只有在绝对必要时才进行复制,从而避免了不必要的复制。而且额外开销也只有一点,在涉及句柄的类库中,这一技术经常用到。

    6.8 讨论

  • 相关阅读:
    玩转nginx的配置文件2
    小程序源码:独家修复登录接口社区论坛微信小程序源码下载-支持多种发帖模式超强社区
    linux 单用户模式、^M 坏的解释器
    Mybatis(整合spring)
    STL-stack、queue和priority_queue的模拟实现
    Spark 和 Kafka 处理 API 请求与返回数据DEMO
    LeetCode每日一题——2558. Take Gifts From the Richest Pile
    jenkins-pipeline集成sonarqube代码扫描
    24.GRASP模式
    iNFTnews | 从《雪崩》到百度“希壤”,元宇宙30年的16件大事
  • 原文地址:https://blog.csdn.net/weixin_44557375/article/details/125493870