shared_ptr 是 Boost 库中的引用计数智能指针。
引用计数的问题是它不能处理循环。我想知道如何在 C++ 中解决这个问题。
请不要提出诸如“不要循环”或“使用弱点”之类的建议。
编辑
我不喜欢只使用 weak_ptr 的建议,因为显然如果你知道你会创建一个循环,那么你就不会有问题。如果您在运行时生成 shared_ptrs,您也无法知道在编译时会有一个循环。
因此,请自行删除其中使用 weak_ptr 的答案,因为我特别要求不要有那种答案......
shared_ptr 是 Boost 库中的引用计数智能指针。
引用计数的问题是它不能处理循环。我想知道如何在 C++ 中解决这个问题。
请不要提出诸如“不要循环”或“使用弱点”之类的建议。
编辑
我不喜欢只使用 weak_ptr 的建议,因为显然如果你知道你会创建一个循环,那么你就不会有问题。如果您在运行时生成 shared_ptrs,您也无法知道在编译时会有一个循环。
因此,请自行删除其中使用 weak_ptr 的答案,因为我特别要求不要有那种答案......
shared_ptr
表示所有权关系。而weak_ptr
代表意识。拥有多个相互拥有的对象意味着您在架构方面存在问题,这可以通过将一个或多个自己的's 更改为意识到's(即weak_ptr
's)来解决。
我不明白为什么建议weak_ptr
被认为是无用的。
我理解您对被轻率地告知使用weak_ptr
来破坏循环引用和我自己的烦恼,当我被告知循环引用是不好的编程风格时,我几乎感到愤怒。
你特别问你如何发现循环引用。事实是,在一个复杂的项目中,一些参考周期是间接的并且难以发现。
答案是你不应该做出让你容易受到循环引用的错误声明。我是认真的,我在批评一种非常流行的做法——盲目地对所有事情使用 shared_ptr。
您应该在设计中清楚哪些指针是所有者,哪些是观察者。
供业主使用shared_ptr
。
供观察者使用weak_ptr
——所有这些,而不仅仅是那些你认为可能是一个循环的一部分。
如果您遵循这种做法,那么循环引用不会引起任何问题,您无需担心它们。当然,当您想使用它们时,您将需要编写大量代码来将所有这些weak_ptr
s 转换为s - Boost 确实无法胜任这项工作。shared_ptr
检测周期相当容易:
然而,它不是很有用。而且通常不可能解决引用计数指针的循环问题——这就是为什么发明了替代垃圾收集方案(如生成清除)的原因。
我还没有找到比绘制大型 UML 图和寻找周期更好的方法。
为了调试,我使用一个实例计数器进入注册表,如下所示:
template <DWORD id>
class CDbgInstCount
{
public:
#ifdef _DEBUG
CDbgInstCount() { reghelper.Add(id, 1); }
CDbgInstCount(CDbgInstCount const &) { reghelper.Add(id, 1); }
~CDbgInstCount() { reghelper.Add(id, -1); }
#else
#endif
};
我只是需要将它添加到有问题的类中,然后查看注册表。
(ID,如果给出例如 'XYZ!' 将被转换为字符串。不幸的是,您不能将字符串常量指定为模板参数)
boost::weak_ptr
和可能的组合boost::shared_ptr
?这篇文章可能很有趣。
请参阅这篇关于在图中检测周期的帖子。
您可能需要像Mark and Sweep这样的垃圾收集器技术。该算法的思想是:
由于您正在使用shared_ptr
任何仍然存在的指针,因此您无法到达应该被视为循环的成员。
下面我描述一个非常幼稚的例子,说明如何实现sweep()
部分算法,但是它将reset()
所有剩余的指针放在收集器上。
此代码存储shared_ptr<Cycle_t>
指针。该类Collector
负责跟踪所有指针并在sweep()
执行时将其删除。
#include <vector>
#include <memory>
class Cycle_t;
typedef std::shared_ptr<Cycle_t> Ref_t;
// struct Cycle;
struct Cycle_t {
Ref_t cycle;
Cycle_t() {}
Cycle_t(Ref_t cycle) : cycle(cycle) {}
};
struct collector {
// Note this vector will grow endlessy.
// You should find a way to reuse old links
std::vector<std::weak_ptr<Cycle_t>> memory;
// Allocate a shared pointer keeping
// a weak ref on the memory vector:
inline Ref_t add(Ref_t ref) {
memory.emplace_back(ref);
return ref;
}
inline Ref_t add(Cycle_t value) {
Ref_t ref = std::make_shared<Cycle_t>(value);
return add(ref);
}
inline Ref_t add() {
Ref_t ref = std::make_shared<Cycle_t>();
return add(ref);
}
void sweep() {
// Run a sweep algorithm:
for (auto& ref : memory) {
// If the original shared_ptr still exists:
if (auto ptr = ref.lock()) {
// Reset each pointer contained within it:
ptr->cycle.reset();
// Doing this will trigger a deallocation cascade, since
// the pointer it used to reference will now lose its
// last reference and be deleted by the reference counting
// system.
//
// The `ptr` pointer will not be deletd on the cascade
// because we still have at least the current reference
// to it.
}
// When we leave the loop `ptr` loses its last reference
// and should be deleted.
}
}
};
然后你可以像这样使用它:
Collector collector;
int main() {
// Build your shared pointers:
{
// Allocate them using the collector:
Ref_t c1 = collector.add();
Ref_t c2 = collector.add(c1);
// Then create the cycle:
c1.get()->cycle = c2;
// A normal block with no cycles:
Ref_t c3 = collector.add();
}
// In another scope:
{
// Note: if you run sweep an you still have an existing
// reference to one of the pointers in the collector
// you will lose it since it will be reset().
collector.sweep();
}
}
我用 Valgrind 对其进行了测试,没有列出内存泄漏或“仍然可访问”的块,因此它可能按预期工作。
关于此实现的一些注意事项:
shared_ptr
(其工作方式类似于引用计数 GC)来实现这样的垃圾收集器,因为标记和清除算法已经完成了这项工作。最后,如果您关心(2),这种实现并非闻所未闻。CPython(Python 的主要实现)确实使用了引用计数和标记和扫描的混合,但主要是出于历史原因。
mark()
功能:要实现该mark()
功能,您需要进行一些修改:
需要向 中添加一个bool marked;
属性Cycle_t
,并使用它来检查指针是否被标记。
您将需要编写如下所示的Collector::mark()
函数:
void mark(Ref_t root) {
root->marked = true;
// For each other Ref_t stored on root:
for (Ref_t& item : root) {
mark(item);
}
}
然后你应该修改sweep()
函数以删除标记,如果指针被标记,否则reset()
指针:
void sweep() {
// Run a sweep algorithm:
for (auto& ref : memory) {
// If it still exists:
if (auto ptr = ref.lock()) {
// And is marked:
if (ptr->marked) {
ptr->marked = false;
} else {
ptr->cycle.reset();
}
}
}
}
这是一个冗长的解释,但我希望它对某人有所帮助。
回答老问题,您可以尝试使用侵入式指针,这可能有助于计算资源被引用的次数。
#include <cstdlib>
#include <iostream>
#include <boost/intrusive_ptr.hpp>
class some_resource
{
size_t m_counter;
public:
some_resource(void) :
m_counter(0)
{
std::cout << "Resource created" << std::endl;
}
~some_resource(void)
{
std::cout << "Resource destroyed" << std::endl;
}
size_t refcnt(void)
{
return m_counter;
}
void ref(void)
{
m_counter++;
}
void unref(void)
{
m_counter--;
}
};
void
intrusive_ptr_add_ref(some_resource* r)
{
r->ref();
std::cout << "Resource referenced: " << r->refcnt()
<< std::endl;
}
void
intrusive_ptr_release(some_resource* r)
{
r->unref();
std::cout << "Resource unreferenced: " << r->refcnt()
<< std::endl;
if (r->refcnt() == 0)
delete r;
}
int main(void)
{
boost::intrusive_ptr<some_resource> r(new some_resource);
boost::intrusive_ptr<some_resource> r2(r);
std::cout << "Program exiting" << std::endl;
return EXIT_SUCCESS;
}
这是返回的结果。
Resource created
Resource referenced: 1
Resource referenced: 2
Program exiting
Resource unreferenced: 1
Resource unreferenced: 0
Resource destroyed
*** Program Exit ***
如果你有带有共享指针的 Cycles,你将会有内存泄漏。因此,如果您转储内存泄漏对象,您可以查看这些类型以找到实际循环。
我认为您确实在要求诸如 Java 的垃圾收集之类的东西。这个问题讨论了一个“自动循环断路器” shared_ptr
。
你可以在你的程序中有shared_ptr
循环并让每个对象释放,但这违背了流行的建议。流行的建议是通过使用参与循环的对象之一来打破循环。shared_ptr
weak_ptr
如果你坚持shared_ptr
在你的程序中保留一个循环,你仍然可以这样做,但你必须在销毁时手动打破循环。shared_ptr
这很像记住手动调用delete
一个对象,所以你可以看到为什么不推荐它。
struct B;
struct A {
shared_ptr<B> b;
void prepForShutdown() {
b = nullptr; // unlink from b.
}
~A() { puts("~A"); }
};
struct B {
shared_ptr<A> a;
~B() { puts("~B"); }
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b = b;
b->a = a;
a->prepForShutdown(); // Break the cycle
// Without this, either dtor cannot run, because A holds a reference
// to b and B holds a reference to A.
a = nullptr;
b = nullptr;
}
我知道你说“没有weak_ptr”,但为什么不呢?让你的头与一个weak_ptr 到尾巴,以及与一个weak_ptr 到头的尾巴将防止循环。