使用std::unordered_set对多个受试者进行观察

Observer with multiple subjects using std::unordered_set

本文关键字:受试者 观察 std unordered set 使用      更新时间:2023-10-16

我见过观察者设计模式的实现,其中观察者负责多个主题。这些实现中的大多数使用std::vector<Subject*>来跟踪受试者。

我有可能做类似的事情,用std::unordered_set<weak_ptr<Subject>>代替吗?

我想使用unordered_set的原因是我不需要重复,也不需要有序的容器。据我所知,在这种情况下,unordered_set是最好的选择。此外,我使用weak_ptr的原因是它应该更安全?

如果您不同意,请留下一个答案,解释我应该使用什么容器。如果我确实使用了unordered_set,我将不得不为weak_ptr声明一个散列函数,但这可以通过仅使用subjects.lock().get()获得的内部指针的散列函数来实现吗?

首先,在我的回答中,我将使用Subject作为向注册观察员发送消息的人,因为这是这两个术语的常见用法。

我可以做类似的事情吗,用std::unordered_set<weak_ptr<Observer>>代替?

这是可能的。然而,请记住,weak_ptr持有的对象可以被释放,在访问底层对象之前,weak_ptr需要被强制转换为shared_ptr。这样做是为了在处理对象时不会释放它

我是否可以做类似的事情,使用std::unordered_set>?

如果你需要强制唯一性,在我看来,unordered_set是一个不错的选择。如果你不需要,那么向量是更直接的解决方案。有些人会说unique_set比向量慢,需要更多的内存,但除非你需要非常高频率的Observer或数千个Observer的注册,否则你不会注意到差异。

关于弱指针,它为您提供了在注册时解除Observer分配的灵活性,所以它应该很好。如果您来自Java这样的内存管理语言,那么这种行为可能会出乎意料。如果你想在主题中注册时保留它们,你可以使用shared_pointer。

我必须为weak_ptr声明一个散列函数,但这可以通过使用observer.lock((.get((获得的内部指针的散列函数来实现吗?

创建哈希函数时要小心,我不建议您使用对象的指针来创建哈希函数,特别是当您的主题可以被复制/移动时。相反,您可以在创建时使用计数器为每个Subject创建一个唯一的标识符,并记住相应地编写复制/移动构造函数和运算符。

如果你不能编写一个识别散列函数,那么你就不应该使用unique_set,因为你失去了它带来的优势。

作为一个脚注,对象容器的美妙之处在于,您可以根据自己的需求进行调整,如果每个解决方案都能满足您的实际需求,那么它就是正确的解决方案。

对于容器的选择,实际上没有一个"正确"的答案;这取决于从性能的角度看你的目标是什么。性能是否真的那么重要。

它还取决于内存效率。如果您只有这些未定义的set对象中的一些,并且需要非常快速的查找,那么它可能是一个不错的选择。由于它是一个哈希表,因此每个unsodered_set对象将使用相当大的内存。

如果你有很多没有定义的对象,但项目很少,那么在内存预算方面可能会有点贵。如果您需要快速插入和移除,那么在这种情况下std::set可能会更好。然而,如果集合只包含少数项目,那么由于处理器缓存的原因,使用std::vector的线性搜索,查找实际上可能会更快(即,与std::set相比,向量元素的引用位置更好,这可能会导致更多元素在同一缓存线上(。vector的内存使用率将低于std::set或std::unordered_set。

如果出于某种原因需要快速查找特定对象,并使用std::vector,并且通常具有适度数量的元素,则可以按排序顺序将项插入到向量中。然后,您可以使用std::lower_bound来执行O(logn(二进制搜索查找。然而,这对于插入和移除元件具有潜在的高成本。

在大多数情况下,我可能只会选择std:vector——你通常很少有观察者,所以不妨严格使用内存。如果这些对象与shared_ptr一起在其他地方使用,那么使用weak_ptr无疑是一个不错的选择。

在Observer模式中,Observer订阅有关Subject更改的通知。受试者负责在其可观察状态发生变化时更新所有订阅的观察者。要做到这一点,观察员不需要跟踪受试者。相反,主题必须跟踪所有订阅的观察员。

观察者模式的一个很好的解释可以在这里找到:https://sourcemaking.com/design_patterns/observer

代码大纲:

class Subject;
class Observer
{
public:
    // when notified about a change, the Observer
    // knows which Subject changed, because of the parameter s
    virtual void subjectChanged(Subject* s)=0;
};
class Subject
{
private:
    int m_internalState;
    std::set<Observer*> m_observers;
public:
    void subscribe(Observer* o)
    {
        m_observers.insert(o);
    }
    void unsubscribe(Observer* o);
    {
        m_observers.erase(o);
    }
    void setInternalState(int state)
    {
        auto end=m_observers.end();
        for(auto it=m_observers.begin(); it != end; ++it)
            it->subjectChanged(this);
    }
};

在大多数情况下,选择哪种确切的集合类型来存储观察者并不重要,因为观察者很少。然而,选择集合类型的优点是,每个观察者将只收到一个通知。对于vector,如果(出于某种原因(订阅了多次,那么同一个观察者可能会收到关于同一更改的多个通知。

我真的认为使用std::unordered_set有点过于致命
这种观察者模式是什么?当一个事件或状态发生变化时,对一组状态检查器进行迭代,并在状态无效或任何类型的特殊情况下使它们执行某些操作。

既然如此,你想用overriden虚拟函数迭代一个包含对象的数组并调用它。为什么set能给我们带来任何好处?此外,我不明白weak_ptr的想法——观察者的所有者是持有它们的数组。该数组的所有者是Subject。

既然已经说了这么多,我就选择std::vector<std::unique_ptr<Observer>>

编辑:

使用C++11,我甚至会使用std::vector<std::function<void(Subject&)>>,避免继承+重写的样板。

最简单的方法是使用boost::signals2,它已经为您实现了所有签名。您的方法的根本问题是,实现与具有特定主题和观察者的特定签名绑定,与适用于所有情况的通用解决方案相比,这几乎毫无价值。

Observer模式不是一个模式,它是一个类模板。