追踪(栈分配的)对象

6
在一个相当大的应用程序中,我想要跟踪某个类的对象的一些统计信息。为了不降低性能,我希望这些统计信息是以拉取配置方式更新的。因此,我需要在某个位置引用每个活动对象。有没有一种习惯用法来:
  1. 创建、搜索、迭代这样的引用
  2. 自动管理它(即在销毁时删除引用)
我在考虑智能指针集合,但内存管理会有点倒置:当销毁对象时,我想要删除智能指针,而不是销毁对象。理想情况下,我不想重新发明轮子。
我可以接受指针删除的延迟,只需快速使其无效即可。
编辑:因为paddy要求这样做:采用拉取式收集的原因是获取信息可能相对昂贵。推送显然是一个干净的解决方案,但被认为太昂贵。

你需要哪些统计数据?如果只是追踪实例数量,可以考虑每个类存储一个原子整数,然后可能制作一个简单的RAII包装器来进行计数(这意味着你只需将其放在类定义中,工作就完成了)。 - paddy
2
如果有用的话,这里有一个简单的例子,说明我所说的。它不允许您迭代或搜索对象,但不清楚为什么要这样做。以这种方式访问另一个堆栈可能不是一个好主意。这个例子只提供了非常低的开销和线程安全的对象计数。 - paddy
更新单个原子会比跟踪所有内存分配并提供按需线程安全的统计信息要便宜得多。 - paddy
2
嗯,你看,我不知道。你并没有告诉我们你想要什么统计数据,所以我不得不假设你想要计数。我确实问过,但你没有回复,所以我认为我已经正确地推断出了你的意图。除非你提供真正的信息,否则我更倾向于将此视为XY问题。 - paddy
我已经发布了一个答案,你可能会发现它有用。我从我的示例中采用了Countable的想法,并将其改为了Trackable。现在,单个堆栈可以将对象推入到可以作为链表迭代的堆栈结构中。它非常轻量级,可能是你正在寻找的开始。一旦你想要从另一个线程使用它,就会出现同步问题,但我相信你可以处理它=) - paddy
显示剩余2条评论
2个回答

2
没有特别的语言功能可以让您这样做。有时,对象跟踪是通过自己的内存分配器来处理的,但在堆栈上很难实现。
但是,如果您只使用堆栈,假设被跟踪的对象在单个线程中,它实际上使问题变得更容易了。C++对于堆栈上构造和销毁的顺序做出了特殊保证。也就是说,销毁顺序正好与构造顺序相反。
因此,您可以利用这一点,在每个对象中存储一个指针,加上一个静态指针来跟踪最新的对象。现在,您拥有一个表示为链表的对象堆栈。
template <typename T>
class Trackable
{
public:
    Trackable()
    : previous( current() )
    {
        current() = this;
    }

    ~Trackable()
    {
        current() = previous;
    }

    // External interface
    static const T *head() const { return dynamic_cast<const T*>( current() ); }
    const T *next() const { return dynamic_cast<const T*>( previous ); }

private:
    static Trackable * & current()
    {
        static Trackable *ptr = nullptr;
        return ptr;
    }

    Trackable *previous;
}

例子:

struct Foo : Trackable<Foo> {};
struct Bar : Trackable<Bar> {};

//  :::

// Walk linked list of Foo objects currently on stack.
for( Foo *foo = Foo::head(); foo; foo = foo->next() )
{
    // Do kung foo
}

现在,诚然这是一个非常简单的解决方案。在一个大型应用程序中,您可能会有多个堆栈使用您的对象。您可以通过使current()使用thread_local语义来处理多个线程上的堆栈。虽然您需要一些魔法才能使其工作,因为head()需要指向线程注册表,这将需要同步。
您绝对不希望将所有堆栈同步到单个列表中,因为这将破坏程序的性能可伸缩性。
至于您的拉取要求,我认为这是一个单独的线程想要遍历列表。您需要一种同步方式,以便在迭代列表时阻止所有新对象构造或销毁在Trackable<T>内部。或类似的操作。
但至少您可以采用这个基本思路并将其扩展到满足您的需求。
请记住,如果您动态分配对象,则不能使用此简单列表方法。对此,您需要一个双向列表。

2

最简单的方法是在每个对象内部放置代码,使其在实例化时注册自己,并在销毁时将自己移除。可以使用CRTP轻松地注入此代码:

template <class T>
struct AutoRef {

    static auto &all() {
        static std::set<T*> theSet;
        return theSet;
    }

private:
    friend T;

    AutoRef() { all().insert(static_cast<T*>(this)); }
    ~AutoRef() { all().erase(static_cast<T*>(this)); }
};

现在一个Foo类可以继承AutoRef<Foo>,以使其实例被引用到AutoRef<Foo>::all()中。 在 Coliru 上查看实现效果

比我的解决方案简单得多 =) 尽管这具有对数复杂度。当他们抱怨每个对象只有一个原子增量和减量是“太昂贵”的时候,我认真对待了他们的意见。 - paddy
此外,我们两个解决方案都存在一个问题,即使在跨线程同步访问的情况下,您也无法保证仍在注册表中的对象当前未被销毁,因此无效。修改这种行为并不难,但这是一个值得考虑的特殊细节。 - paddy
@paddy OP不想急切地推送统计数据,因为它们生成速度较慢,在大多数情况下被忽略。没有提到计数或多线程。容器可以根据使用模式(插入/删除率、查询数量等)轻松更换为另一个容器。 - Quentin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接