首页 文章

设计(shared_ptr weak_ptr)与原始指针兼容

提问于
浏览
0

序言

在C 11中有 std::shared_ptr std::weak_ptr 组合 . 虽然非常有用,但它有一个令人讨厌的问题:你cannot easily construct shared_ptr from a raw pointer . 由于这个缺陷,这些智能指针通常会变成"viral":人们开始完全避免原始指针和引用,并在代码中使用shared_ptr和weak_ptr智能指针 . 因为没有办法将原始引用传递给期望智能指针的函数 .

另一方面,有 boost::intrusive_ptr . 它相当于 std::shared_ptr ,可以很容易地从原始指针构造,因为引用计数器包含在对象中 . 不幸的是,它没有weak_ptr伴侣,所以没有办法让你可以检查无效的非拥有引用 . 事实上,有些人认为weak companion for intrusive_ptr is impossible .

现在,有 std::enable_shared_from_thisembeds a weak_ptr直接进入你的类,这样你就可以从指针到对象构造shared_ptr . 但是存在一些小的限制(至少必须存在一个shared_ptr),并且它仍然不允许使用明显的语法: std::shared_ptr(pObject) .

还有一个 std::make_shared ,其中allocates reference counters and the user's object in a single memory chunk . 这非常接近intrusive_ptr的概念,但用户的对象可以独立于引用计数块而被销毁 . 此外,这个概念有一个不可避免的缺点:只有当所有weak_ptr-s都消失时才会释放整个内存块(可能很大) .

问题

主要问题是:如何创建一对shared_ptr / weak_ptr,这将有 std::shared_ptr / std::weak_ptrboost::intrusive_ptr 的好处?

特别是:

  • shared_ptr模型对对象共享所有权,即当指向它的最后一个shared_ptr被销毁时,该对象被完全销毁 .

  • weak_ptr不会对对象建模所有权,它可以用于解决循环依赖问题 .
    可以检查

  • weak_ptr是否有效:当存在指向该对象的shared_ptr时,它是有效的 .

  • shared_ptr可以从有效的weak_ptr构造 .

  • weak_ptr可以从对象的有效原始指针构造 . 如果至少存在一个仍指向该对象的weak_ptr,则原始指针有效 . 从无效指针构造weak_ptr会导致未定义的行为 .

  • 整个智能指针系统应该是友好的,就像上面提到的现有系统一样 .

侵入是可以的,即要求用户从给定的基类继承一次 . 当对象已经被破坏时保持对象的内存也是可以的 . 线程安全是非常好的(除非效率太低),但没有它的解决方案也很有趣 . 可以为每个对象分配几个内存块,尽管每个对象有一个内存块是首选 .

3 回答

  • 0
    • 点1-4和6已经由shared_ptr / weak_ptr建模 .

    • 第5点毫无意义 . 如果共享生命周期,那么如果 weak_ptr 存在但 shared_ptr 不存在则没有有效对象 . 任何原始指针都是无效指针 . 对象的生命周期已经结束 . 对象已不复存在 .

    weak_ptr 不会使对象保持活动状态,它会使控制块保持活动状态 . shared_ptr 使控制块和受控对象保持活动状态 .

    如果你没有't want to 1250025 memory by combining the control block with the controlled object, don'拨打 make_shared .

    如果您不希望 shared_ptr<X> 以病毒式传递给函数,请不要传递它 . 将引用或const引用传递给 X . 如果您打算在函数中管理生命周期,则只需在参数列表中提及 shared_ptr . 如果您只想对shared_ptr指向的内容执行操作,请传递 *p*p.get() 并接受[const]引用 .

  • 1

    覆盖对象上的 new 以在对象实例之前分配控制块 .

    这是伪侵入的 . 由于已知的偏移,可以从原始指针转换为 . 可以毫无问题地销毁对象 .

    引用计数块包含强弱计数,以及用于销毁对象的函数对象 .

    缺点:它不能很好地工作在多态 .

    想象一下,我们有:

    struct A {int x;};
    struct B {int y;};
    struct C:B,A {int z;};
    

    然后我们用这种方式分配 C .

    C* c = new C{};
    

    并将其存储在_1250037中:

    A* a = c;
    

    然后我们将它传递给智能指针到A . 它期望控制块在地址 a 指向之前,但因为 B 在继承 A 之前存在 C 的图形,有一个 B 的实例 .

    这似乎不太理想 .


    所以我们作弊 . 我们再次取代 new . 但它改为使用注册表在某处注册指针值和大小 . 我们存储弱/强指针计数(等) .

    我们依赖于线性地址空间和类布局 . 当我们有一个指针 p 时,我们只需查找它所在的地址范围 . 然后我们知道强/弱计数 .

    这个通常具有可怕的性能,尤其是多线程,并且依赖于未定义的行为(指针比较指针不指向同一个对象,或者在这种情况下为 less 顺序) .

  • 3

    从理论上讲,可以实现 shared_ptrweak_ptr 的侵入版本,但由于C语言的限制,它可能不安全 .

    两个引用计数器(强和弱)存储在托管对象的基类 RefCounters 中 . 任何智能指针(共享或弱)都包含指向托管对象的单个指针 . 共享指针拥有对象本身,共享弱指针一起拥有对象的内存块 . 因此,当最后一个共享指针消失时,对象被销毁,但只要存在至少一个指向它的弱指针,它的内存块就会保持活动状态 . 鉴于所有涉及的类型仍然从 RefCounted 类继承,所以转换指针按预期工作 .

    不幸的是,在C中,在对象被销毁之后通常禁止与对象成员一起工作,尽管大多数实现应该允许这样做而没有问题 . 有关该方法易读性的更多详细信息,请参见this question .

    以下是智能指针工作所需的基类:

    struct RefCounters {
        size_t strong_cnt;
        size_t weak_cnt;
    };
    struct RefCounted : public RefCounters {
        virtual ~RefCounted() {}
    };
    

    这是共享指针定义的一部分(显示如何销毁对象并释放内存块):

    template<class T> class SharedPtr {
        static_assert(std::is_base_of<RefCounted, T>::value);
        T *ptr;
    
        RefCounters *Counter() const {
            RefCounters *base = ptr;
            return base;
        }
        void DestroyObject() {
            ptr->~T();
        }
        void DeallocateMemory() {
            RefCounted *base = ptr;
            operator delete(base);
        }
    
    public:
        ~SharedPtr() {
            if (ptr) {
                if (--Counter()->strong_cnt == 0) {
                    DestroyObject();
                    if (Counter()->weak_cnt == 0)
                        DeallocateMemory();
                }
            }
        }
        ...
    };
    

    带有样本的完整代码可用here .

相关问题