【问题标题】:Design of (shared_ptr + weak_ptr) compatible with raw pointers(shared_ptr + weak_ptr) 设计兼容原始指针
【发布时间】:2017-05-06 20:38:37
【问题描述】:

序言

在 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_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的好处?

特别是:

  1. shared_ptr 对对象的共享所有权建模,即对象在指向它的最后一个 shared_ptr 被销毁时被销毁。
  2. weak_ptr 不对对象的所有权建模,它可用于解决循环依赖问题。
  3. 可以检查weak_ptr是否有效:当存在指向对象的shared_ptr时有效。
  4. shared_ptr 可以从有效的weak_ptr 构造。
  5. weak_ptr 可以从指向对象的有效原始指针构造。如果存在至少一个仍然指向该对象的弱指针,则原始指针是有效的。从无效指针构造weak_ptr 会导致未定义的行为。
  6. 整个智能指针系统应该像上面提到的现有系统一样易于转换。

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

【问题讨论】:

  • en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr 显示了许多基于原始指针的构造函数,链接只是指向一个陷阱,即从一个原始指针中生成更多的 shared_ptr 将有许多不同的 shared_ptr 实例,所有人都想删除对象时需要。您必须制作一个 shared_ptr 然后复制它。这可能会令人讨厌,但不会使这个概念无法使用。
  • @stefaanv:毫无疑问,std::shared_ptr + std::weak_ptr 非常好用,我自己也成功使用过。但是,这个问题旨在呼吁更好的设计,它没有有这种“麻烦”。
  • 侵入式指针目标和从 enable_shared_from_this 派生的类之间的病毒式传播有何不同?它们都需要了解其生命周期管理机制。
  • @stgatilov:我理解,但我想澄清一下,没有链接阅读问题似乎无法完成或导致更大的问题。确实,最好不要在这个问题中运行。但是,在工作 shared_ptr 时,应该在生命周期管理完成时完成,所以在使用原始指针时立即而不是在某个时间点完成。当需要传递给函数(而不是存储或传递给线程)时,可以从共享指针中检索原始指针。
  • 如果你遵循gotw-91-solution-smart-pointer-parameters/的指导方针,智能指针的病毒性是有限的。

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


【解决方案1】:
  • 点 1-4 和 6 已由 shared_ptr/weak_ptr 建模。

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

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

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

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

【讨论】:

  • 如果控制块被插入到对象本身中(或分配在附近),那么应该可以从对象指针中获取它的地址。这样就可以从原始指针构造weak_ptr,尽管对象已经被销毁了。
  • @stgatilov 我知道你要去哪里,但这仍然不能满足 5。因为如果控制块是对象的一部分,那么当对象存在时它将不复存在。没有weak_ptr的可能性。现有的 shared_ptr/weak_ptr 代表了最先进的技术。作为 boost 库的一部分,它已被证明多年。这些侵入性/非侵入性的争论不时出现。我们仍在使用 shared_ptr,因为它可以工作并且没有找到更好的方法。
  • 好吧,我确实不是语言律师。当您销毁引用计数器对象时,它的原始类型字段不会改变。因此,即使引用计数器是对象的一部分,它们也可以在对象破坏中幸存下来并留在仍然拥有的内存块中。无论如何,如果 C++ 标准明确禁止此类事情,则可以像 std::make_shared 那样分配额外的空间。也许我应该从一开始就用“intrusive_ptr with weak_ptr”这样的问题来表达......
  • @stgatilov 当你销毁一个对象时,子对象会在语义上被销毁。即使有琐碎的类型并且存储仍然存在。因此,您将无法在销毁后访问子对象。而且你不能让一个对象驻留在另一个对象的中间。 C++ 并不是真正的低级语言。
  • 虽然你可能认为你可以通过放置足够的volatile 来摆脱语义 UB 1) 它很丑 2) 遗憾的是编译器没有将 volatile 变量放在寄存器中,所以它效率低下 3) 你甚至不能在这里讨论,模组不接受使用volatile
【解决方案2】:

覆盖对象上的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 指向的地址之前,但是因为BC 的继承图中存在于A 之前,所以那里有一个B 的实例。

这似乎不太理想。


所以我们作弊。我们再次替换new。但它反而在某处的注册表中注册了指针值和大小。我们在那里存储弱/强指针计数(等)。

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

一般来说,这个性能很糟糕,尤其是多线程,并且依赖于未定义的行为(指针比较不指向同一个对象,或less这种情况下的顺序)。

【讨论】:

  • +1 用于实际回答原始问题。重载operator new 只允许使用new T() 语法而不是make_shared<T>()。至于最后存储每个类的地址范围的想法,这当然是一派胡言=)
  • 继承问题在某种程度上存在于侵入式智能指针的思想中。如果您确保任何托管类 T 从某个空的 Base 类继承一次,那么您很可能可以将指针静态转换为 Base* 以获取对象实际开始的地址。
  • @stgatilov 它必须继承自 Base 作为其层次结构中的 first 类型(左上角)。并且 ABI 不能在该指针值之前放置任何东西(如 vtable)。它非常脆弱。从某种意义上说,存储范围不是废话。你控制分配。如果内存是线性且平坦的(使用char*s),那么你很好。
【解决方案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

【讨论】:

    猜你喜欢
    • 2013-08-18
    • 2013-10-09
    • 2021-11-15
    • 1970-01-01
    • 2012-09-07
    • 1970-01-01
    • 2021-11-05
    • 2013-05-21
    • 1970-01-01
    相关资源
    最近更新 更多