设计(shared_ptr + weak_ptr)与原始指针

时间:2016-12-21 12:07:44

标签: c++ c++11 boost shared-ptr smart-pointers

序言

在C ++ 11中,有std::shared_ptr + std::weak_ptr组合。尽管非常有用,但它有一个令人讨厌的问题:你cannot easily construct shared_ptr from a raw pointer。由于这个缺陷,这些智能指针通常变得“病毒”:人们开始完全避免原始指针和引用,并在代码中使用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_sharedallocates 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的好处?

特别是:

  1. shared_ptr建模对象的共享所有权,即当指向它的最后一个shared_ptr被销毁时,该对象被完全销毁。
  2. weak_ptr不会对对象建模所有权,它可以用来解决循环依赖问题。
  3. 可以检查weak_ptr是否有效:当存在指向该对象的shared_ptr时它是有效的。
  4. shared_ptr可以从有效的weak_ptr构建。
  5. weak_ptr可以从对象的有效原始指针构造。如果至少存在一个仍指向该对象的weak_ptr,则原始指针有效。从无效指针构造weak_ptr会导致未定义的行为。
  6. 整个智能指针系统应该是适合投射的,就像上面提到的现有系统一样。
  7. 侵入是可以的,即要求用户从给定的基类继承一次。当对象已经被破坏时保持对象的内存也是可以的。线程安全是非常好的(除非效率太低),但没有它的解决方案也很有趣。可以为每个对象分配几个内存块,但每个对象最多只有一个内存块。

3 个答案:

答案 0 :(得分:3)

  • 第1-4和第6点已经由shared_ptr / weak_ptr建模。

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

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

如果您不想通过将控制块与受控对象组合来“浪费”内存,请不要调用make_shared

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

答案 1 :(得分:1)

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

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

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

缺点:它不能很好地适用于多态。

想象一下我们有:

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

然后我们以这种方式分配C

C* c = new C{};

并将其存储在A*

A* a = c;

然后我们将它传递给智能指针到A。它期望控制块在地址a指向的位置之前,但由于B存在于A的继承图中的C之前,因此存在{ {1}}而是。

这似乎不太理想。

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

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

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

答案 2 :(得分:0)

理论上,可以实现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