【问题标题】:Adding and removing items without invalidating iterators在不使迭代器失效的情况下添加和删除项目
【发布时间】:2009-06-08 21:54:58
【问题描述】:

我有一个包含“观察者”列表的对象。这些观察者会收到有关事物的通知,并且他们可能会通过在对象中添加或删除自己或其他观察者来响应此更改。

我想要一种强大的,而不是不必要的缓慢的方式来支持这一点。

class Thing {
public:
    class Observer {
    public:
        virtual void on_change(Thing* thing) = 0;
    };
    void add_observer(Observer* observer);
    void remove_observer(Observer* observer);

    void notify_observers();
private:
    typedef std::vector<Observer*> Observers;
    Observers observers;
};

void Thing::notify_observers() {

    /* going backwards through a vector allows the current item to be removed in
    the callback, but it can't cope with not-yet-called observers being removed */
    for(int i=observers.size()-1; i>=0; i--)
        observers[i]->on_change(this);

// OR is there another way using something more iterator-like?

    for(Observers::iterator i=...;...;...) {
        (*i)->on_change(this); //<-- what if the Observer implementation calls add_ or remove_ during its execution?
    }
}

我可能有一个标志,由 add_ 和 remove_ 设置,如果它被无效,则重置我的迭代器,然后可能在每个观察者中设置一个“生成”计数器,以便我知道我是否已经调用它?

【问题讨论】:

  • 请注意:您不止一次将“observer”拼错为“observer”。如果您在编译时没有注意到,这可能会导致一些令人头疼的问题。
  • 一个 hacky 解决方法是使指针为 NULL,然后在所有地方进行 NULL 检查。这样你就不需要删除它了。
  • Lodle - 使用 [] 运算符而不是迭代器来处理添加,让它成为一个实际的答案,我很有可能会接受它! O(n) 甜

标签: c++ iterator std


【解决方案1】:

也许您可以使用更好的(?)设计。例如,您可以让通知函数根据它们的返回值将它们删除(或执行任何其他操作),而不是让观察者自己删除。

【讨论】:

  • +1。起初我觉得这有点粗鲁。在考虑了一段时间的影响和可能的解决方案之后,允许每个观察者添加或删除自己或任何其他观察者的“免费”政策有很多你应该考虑的影响。 notify 方法的结果取决于执行和实现的顺序,例如,一个观察者可以删除另一个可能(或没有)已经被通知的观察者。也许您应该回到绘图板上,解释您的真正需求以及为什么需要这种灵活性。
  • 如果需要添加多个观察者怎么办?
【解决方案2】:

添加或插入项目是否会使某些迭代器失效完全取决于容器类型。

您可能需要调查std::list,因为这是对迭代器验证更宽容的容器之一。例如,在移除一个元素时,只有指向被移除元素的迭代器才会失效。所有其他迭代器仍然有效。

您仍然需要确定哪种操作是有效的。您可以考虑不允许在观察者列表上直接添加/删除操作,并在发生通知时将添加和删除操作排队,在通知完成时对队列进行操作。

如果只允许观察者移除自己或添加新的观察者,这可能是矫枉过正,这样的循环将足够安全:

for( std::list<Observer>::iterator i = observers.begin(); i != observers.end(); )
{
    std::list<Observer>::iterator save = i++;
    save->on_change();
}

【讨论】:

  • 您的意思是在 remove_obsever() 方法中有一个“current_observer”成员需要注意?但是如何从中恢复,或者在添加到尾/头之前删除列表的尾/头,所有这些都在返回到 notify_observers() 循环之前?
  • 使您的迭代变量成为类成员是确保任何操作都可行的一种方法。这样,如果您的调用堆栈包含对 on_change 方法的 add 或 remove 函数的回调,您可以修改它并确保迭代变量永远不会失效。这将是一个非常不寻常的设计,您可能可以对您的客户端设置一些合理的限制,以避免需要这样做。
【解决方案3】:

拥有不会失效的迭代器的最简单方法是将观察者存储在列表中而不是向量中。列表迭代器不会因添加或删除项目而失效,除非它们指向要删除的项目。

如果你想坚持使用向量,我能想到的最好方法是在添加项目时重置一个标志(添加会使向量中的每个项目无效),然后使用预减量循环遍历向量(因为删除只会使该点之后的项目无效,而不是之前的项目)。

【讨论】:

    【解决方案4】:

    管理这种混乱的明智方法是设置一个标志,以便删除代码知道它是否在迭代观察者。

    在remove中,如果代码在一个迭代中,那么指针被设置为null而不是remove。该标志被设置为第三种状态以表明这已经发生了。

    观察者必须使用 [] 操作符进行迭代,以防在迭代过程中调用 add 并重新分配数组。忽略数组中的空值。

    迭代后,如果设置标志位表示在迭代中移除了观察者,则可以压缩数组。

    【讨论】:

      【解决方案5】:

      我认为您与几代人走在正确的轨道上。您的问题尚不清楚是否需要将观察者的更改应用于当前通知。如果不是,那么我会将所有需要继续应用到下一代的观察者移到下一代,而不要理会当前的迭代器。

      【讨论】:

        【解决方案6】:

        如果不使任何迭代器指向或超出您已删除的项目,您将无法安全地从向量中添加和删除项目。如果这对您来说是个问题,也许您应该使用不同的容器?您可以添加和删除列表或地图,仅在受影响的位置使迭代器无效。

        您可以使用以下方法进行迭代。它允许在容器中任意插入和删除,因为我们正在制作副本:

        void Thing::notify_observers()
        {
           Observers obscopy=observers;
           Observers::iterator i=obscopy.begin();
           while (i!=obscopy.end())
           {
               (*i)->on_change(this);
               ++i;
           }
        }
        

        【讨论】:

        • 添加到向量可以使向量的所有迭代器无效,因为它可能导致向量重新分配到新的内存块
        • buggy - 但是如果对于副本中的每个项目,您确保它仍然在真正的观察者列表中,它将工作 O(n^2)
        • @Will 我不认为代码有错误:当您通知观察者时,您可能希望冻结通知之前的情况。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-10
        • 2020-06-11
        • 2020-06-22
        • 1970-01-01
        相关资源
        最近更新 更多