【问题标题】:How is the std::tr1::shared_ptr implemented?std::tr1::shared_ptr 是如何实现的?
【发布时间】:2012-03-01 07:43:14
【问题描述】:

我一直在考虑使用共享指针,我知道如何自己实现一个--不想这样做,所以我正在尝试std::tr1::shared_ptr,我有几个问题...

引用计数是如何实现的?它使用双向链表吗? (顺便说一句,我已经用谷歌搜索了,但我找不到任何可靠的东西。)

使用std::tr1::shared_ptr有什么陷阱吗?

【问题讨论】:

  • “它使用双向链表吗?” ——为了什么?这对引用计数有何帮助?
  • 链表有什么帮助?我还以为你说你知道如何自己实现一个。 shared_ptr 如何引用计数是实现定义的,所以这个问题没有答案。您可以随时查看实现的源代码。
  • 第一:有源代码。第二:有 C++0x/11 为什么是 TR1?第三:SO is-a 很棒的社区。 C++ has-a 很棒的社区。海湾合作委员会希望他们拥有它:)(半开玩笑)
  • STL 做了一个whole episode about that。我想说,这很重要,但这主要是因为原子引用更新和高级抽象。
  • @KonradRudolph 您可以使用链表实现引用计数指针。不是集中存储引用的数量,而是存储所有当前指针的链表。当该列表中没有任何内容时,您知道删除该对象。我相信它可能在多线程代码中具有某些优势,因为您可以编写可能无锁的版本。但是我从未见过它真正做到过,所以我猜它在实践中并没有更好。

标签: c++ shared-ptr tr1


【解决方案1】:

shared_ptr 类通常包含两个成员:T*(由operator-> 返回并在operator* 中取消引用)和aux*,其中aux 是一个内部抽象类,包含:

  • 一个计数器(在复制分配/销毁时增加/减少)
  • 使增量/减量原子化所需的一切(如果特定平台原子 INC/DEC 可用,则不需要)
  • 摘要virtual destroy()=0;
  • 一个虚拟析构函数。

这样的aux 类(实际名称取决于实现)是由一系列模板化类派生的(根据显式构造函数给出的类型进行参数化,比如从T 派生的U),添加:

  • 指向对象的指针(与T* 相同,但具有实际类型:这是正确管理T 的所有情况所必需的,因为U 在派生中有多个T层次结构)
  • deletor 对象的副本作为删除策略提供给显式构造函数(或默认的 deletor 只是执行删除 p,其中 p 是上面的 U*
  • destroy 方法的覆盖,调用删除器函子。

一个简化的草图可以是这个:

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

如果需要weak_ptr 互操作性,aux 中需要第二个计数器 (weak_count)(将按 weak_ptr 递增/递减),并且 delete pa 必须仅在两个计数器都达到零时发生。

【讨论】:

  • 无论weak_count如何,如果强引用计数达到零,delete pa不应该发生吗?
  • pa 是指向包含计数器和删除器的结构的指针。当强计数变为零时,您必须删除对象,但计数器必须保持直到所有弱计数器都消失:归零的强计数器使弱计数器知道它们指向被破坏的对象,而弱计数器是仍然需要知道的弱者的数量。
  • 另外,您能否更详细地解释一下为什么需要单独的模板参数 U?
  • @btshengsheng:是的!是的,它必须是pt,已修复。之所以需要“U”,主要是因为 T 指向的可能不是整个对象,而只是其中的一部分(例如一个基类),而正确删除一个对象是获取对象类型的一种方式是需要的。现在,如果对象是运行时多态的(具有虚拟析构函数),删除pt 也会导致调用~U,但如果不是,则没有运行时方法可以从 T* 转到 U*。但是由于对象的静态类型在构造时就知道了,我们可以使用多态内部数据来保证 U 类型的安全。
  • @kevinarpe:在这个严格的样本编号中。 dec 在 dtor 中调用(因此您将永远没有机会进一步使用该值)或在赋值中(pa 即将采用另一个值)
【解决方案2】:

引用计数是如何实现的?

可以使用policy-based class design1将智能指针实现解构为:

  • 存储策略

  • 所有权政策

  • 转化政策

  • 检查政策

作为模板参数包含在内。 流行的所有权策略包括:深度复制、引用计数、引用链接和破坏性复制。

引用计数跟踪指向(拥有2)同一对象的智能指针的数量。当数字变为零时,指针对象被删除3。实际的计数器可能是:

  1. 在智能指针对象之间共享,其中每个智能指针都保存一个指向引用计数器的指针:

  1. 仅包含在为指针对象添加额外间接级别的附加结构中。在这里,在每个智能指针中保存一个计数器的空间开销与访问速度较慢进行了交换:

  1. 包含在指针对象本身中:侵入式引用计数。缺点是必须先验地构造对象,并提供计数功能:

  2. 最后,您问题中的方法,使用双向链表的引用计数称为引用链接,它:

...[1]依赖于您实际上并不需要指向一个指针对象的智能指针对象的实际计数的观察;您只需要检测该计数何时降至零。这导致了保留“所有权列表”的想法:

引用链接相对于引用计数的优势在于前者不使用额外的空闲存储,这使得它更加可靠:创建引用链接的智能指针不会失败。缺点是 该引用链接需要更多内存来记账(三个指针而不是一个指针加一个整数)。此外,引用计数应该更快一些——当你复制智能指针时,只需要一个间接和一个增量。列表管理稍微复杂一些。综上所述, 仅当免费商店稀缺时才应使用参考链接。否则,更喜欢引用计数。

关于你的第二个问题:

它 (std::shared_ptr) 是否使用双向链表?

我能在 C++ 标准中找到的只有:

20.7.2.2.6 shared_ptr 创建
...
7. [注意:这些函数通常会分配比sizeof(T)更多的内存,以允许内部簿记结构,例如引用计数。 ——尾注]

在我看来,这不包括双向链表,因为它们不包含实际计数。

你的第三个问题:

使用std::shared_ptr有什么陷阱吗?

引用管理无论是计数还是链接都是被称为循环引用的资源泄漏的受害者。让我们有一个对象 A 持有指向对象 B 的智能指针。此外,对象 B 持有指向 A 的智能指针。这两个对象形成循环引用;即使您不再使用它们中的任何一个,它们也会相互使用。引用管理策略无法检测到这样的循环引用,两个对象永远保持分配状态。

因为shared_ptr 的实现使用引用计数,循环引用可能是个问题。可以通过更改代码来破坏循环的shared_ptr 链,以便引用之一是weak_ptr。这是通过在共享指针和弱指针之间分配值来完成的,但是弱指针不会影响引用计数。如果指向对象的唯一指针是弱指针,则该对象被销毁。


1.如果制定为策略,则每个设计功能都具有多种实现。

2。智能指针与指向以new 分配的对象的指针类似,不仅指向该对象,还负责其销毁和释放其占用的内存。

3。没有其他问题,如果没有使用其他原始指针和/或指向它。

[1] 现代 C++ 设计:应用通用编程和设计模式。安德烈亚历山德雷斯库,2001 年 2 月 1 日

【讨论】:

    【解决方案3】:

    如果您想查看所有血腥细节,可以查看 boost shared_ptr 实现:

    https://github.com/boostorg/smart_ptr

    引用计数似乎通常使用计数器和特定于平台的原子递增/递减指令或使用互斥锁的显式锁定来实现(参见detail namespace 中的atomic_count_*.hpp 文件)。

    【讨论】:

      【解决方案4】:

      使用std::tr1::shared_ptr有什么陷阱吗?

      是的,如果您在共享内存指针中创建循环,那么当最后一个指针超出范围时,智能指针管理的内存将不会被回收,因为仍然存在对指针的引用(即循环导致引用计数不会下降到零)。

      例如:

      struct A
      {
          std::shared_ptr<A> ptr;
      };
      
      std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
      std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
      shrd_ptr_1->ptr = shrd_ptr_2;
      shrd_ptr_2->ptr = shrd_ptr_1;
      

      现在,即使shrd_ptr_1shrd_ptr_2 超出范围,它们管理的内存也不会被回收,因为它们的ptr 成员都指向彼此。虽然这是这种内存循环的一个非常幼稚的示例,但如果您在没有任何纪律的情况下使用这些类型的指针,它可能会以更加邪恶和难以跟踪的方式发生。例如,我可以看到在哪里尝试实现一个循环链表,其中每个next 指针都是一个std::shared_ptr,如果你不太小心的话,可能会导致问题。

      【讨论】:

      • 如果你适当地使用weak_ptr,这并不是一个真正的陷阱......更多你需要注意的事情。
      • @Matt weak_ptr 不是对循环依赖的修复。解决糟糕设计的唯一方法是更改​​设计,而不是使用其他工具。您不能只取一个拥有的智能指针(或“强引用”),将其替换为非拥有的智能指针(或“弱引用”)并期望程序正常工作。
      • @curiousguy 我的意思是,你可以使用weak_ptr 来修复循环依赖。因此,如果您有一个依赖关系,其中 A 持有一个 shared_ptr 到 B 并且 B 持有一个 shared_ptr 到 A,你可以改为让 B 持有一个 weak_ptr 到 A,如果 B 真的不应该拥有 A 但需要安全地引用它。但这实际上是“改变设计”所以是的,你不能只是盲目地这样做并期望解决所有问题。我的观点是,这不是shared_ptr 的陷阱,shared_ptr 实际上很好,因为它有weak_ptr 作为避免循环依赖的工具。
      • @Matt 是的;但是weak_ptr 很少有用(当时非常有用)。它很有用 f.ex。当您想要将缓存保留在可以接受的项目以死状态存在时;或者在游戏中,怪物可以以“不再存在”状态存在于列表中,等等。但是,它显然在通常的循环依赖情况下没有用,在这种情况下,你有强大的“拥有”链接,如图表,因为这些设计上的链接不能以死状态存在。 “弱” ref 可以轻松解决循环依赖问题的建议是可憎的。
      猜你喜欢
      • 1970-01-01
      • 2010-10-03
      • 1970-01-01
      • 1970-01-01
      • 2011-10-29
      • 1970-01-01
      • 2012-06-11
      • 2011-08-01
      • 2014-09-15
      相关资源
      最近更新 更多