在C ++中是否存在一种惯用的方法来防止运行一系列操作导致集合发生变异的情况?

时间:2017-12-27 17:41:49

标签: c++ raii

假设您有一个包含某种可调用对象集合的类foofoo有一个成员函数run(),它遍历集合并调用每个函数对象。 foo还有一个成员remove(...),它将从集合中删除可调用对象。

是否有一个惯用的,RAII风格的后卫,你可以放入foo.run()foo.remove(...),以便通过调用foo.run()来驱动删除  将被推迟到守卫的破坏者开火?可以用标准库中的东西来完成吗?这种模式有名字吗?

我目前的代码似乎不太优雅,所以我正在寻找最佳实践类型的解决方案。

注意:这不是关于并发性的。非线程安全的解决方案很好。问题在于重新进入和自我参照。

这是一个问题的例子,没有优雅的“延迟删除”警卫。

class ActionPlayer
{
private:
    std::vector<std::pair<int, std::function<void()>>> actions_;
public:
    void addAction(int id, const std::function<void()>& action)
    {
        actions_.push_back({ id, action });
    }

    void removeAction(int id)
    {
        actions_.erase(
            std::remove_if(
                actions_.begin(),
                actions_.end(),
                [id](auto& p) { return p.first == id; }
            ),
            actions_.end()
        );
    }

    void run()
    {
        for (auto& item : actions_) {
            item.second();
        }
    }
};

然后在其他地方:

...

ActionPlayer player;

player.addAction(1, []() {
    std::cout << "Hello there" << std::endl;
});

player.addAction(42, [&player]() {
    std::cout << "foobar" << std::endl;
    player.removeAction(1);
});

player.run(); // boom

编辑......好吧,我可以通过RAII锁定对象看到这样做。假设递归最终终止(如果不是用户的错误),以下应该处理在运行中运行的抛出和重入调用的操作。我使用了缓存的std :: functions,因为在这段代码的实际版本中,addAction和removeAction的等价物是模板函数,它们只能存储在一个vanilla homogeneously类型的容器中。

class ActionPlayer
{
private:

    std::vector<std::pair<int, std::function<void()>>> actions_;
    int run_lock_count_;
    std::vector<std::function<void()>> deferred_ops_;

    class RunLock
    {
    private:
        ActionPlayer* parent_;
    public:
        RunLock(ActionPlayer* parent) : parent_(parent) { (parent_->run_lock_count_)++; }
        ~RunLock()
        {
            if (--parent_->run_lock_count_ == 0) {
                while (!parent_->deferred_ops_.empty()) {
                    auto do_deferred_op = parent_->deferred_ops_.back();
                    parent_->deferred_ops_.pop_back();
                    do_deferred_op();
                }
            }
        }
    };

    bool isFiring() const
    {
        return run_lock_count_ > 0;
    }

public:
    ActionPlayer() : run_lock_count_(0)
    {
    }

    void addAction(int id, const std::function<void()>& action)
    {
        if (!isFiring()) {
            actions_.push_back({ id, action });
        } else {
            deferred_ops_.push_back(
                [&]() {
                    addAction(id, action);
                }
            );
        }
    }

    void removeAction(int id)
    {
        if (!isFiring()) {
            actions_.erase(
                std::remove_if(
                    actions_.begin(),
                    actions_.end(),
                    [id](auto& p) { return p.first == id; }
                ),
                actions_.end()
            );
        } else {
            deferred_ops_.push_back(
                [&]() {
                    removeAction(id); 
                }
            );
        }
    }

    void run()
    {
        RunLock lock(this);
        for (auto& item : actions_) {
            item.second();
        }
    }
};

3 个答案:

答案 0 :(得分:1)

通常的方法是创建vector的副本。但这可能会导致删除的操作再次运行。

void run()
{
    auto actions_copy{actions_};
    for (auto& item : actions_copy) {
        item.second();
    }
}

不允许运行已删除的操作的其他选项

  1. 如果删除了某些操作,则添加bool到商店
  2. 使用shared / weak ptr
  3. 如果知道不会删除当前操作,请使用std::list

答案 1 :(得分:1)

run添加一个标记,表示您通过actions_枚举。然后,如果在设置了该标志的情况下调用removeAction,则将id存储在向量中以便以后删除。您可能还需要一个单独的向量来保存枚举时添加的操作。一旦你完成了actions_的迭代,就可以删除那些想要删除的内容并添加已添加的内容。

这样的东西
// within class ActionPlayer, add these private member variables
private:
    bool running = false;
    std::vector<int> idsToDelete;

public:
    void run() {
        running = true;
        for (auto& item : actions_) {
            item.second();
        }
        running = false;
        for (d: idsToDelete)
            removeAction(d);
        idsToDelete.clear();
    }

    // ...

您可以对延迟addAction调用进行类似的更改(如果任何操作可以添加操作,您需要执行此操作,因为添加可能会导致向量分配更多存储空间,从而无效向量的所有迭代器。)

答案 2 :(得分:0)

我会略微修改结构。我不会直接修改ActionPlayer,而是通过外部修饰符类强制进行所有修改。在这个例子中,我使它成为一个抽象的Modifier类,它可以有不同的具体实现(例如DeferredModifierInstantModifierNullModifierLoggedModifierTestModifier .etc。 )。您的操作现在只需要引用修饰符的抽象基类并调用任何添加/删除.etc。必要时对此进行处理。这允许将修改策略与动作实现分离,并将不同的修改策略注入动作。

这也应该允许更简单地支持并发修改,因为您不再需要切换运行/未运行状态以推迟修改。

此示例显示了按顺序重放动作的简单方法(这是我假设您要维护的属性)。更高级的实现可以向后扫描修改列表,删除所有添加/删除对,然后对修改/删除进行分组,以最小化修改操作列表时的复制。

类似的东西:

class ActionPlayer {
friend class Modifier;
...

    void run(Modifier &modifier) { }
private:
    void addAction(...) { ... }
    void removeAction(...) { ... }
}

class Modifier
{
public:
    virtual ~Modifier() {}
    virtual addAction(...) = 0;
    virtual removeAction(...) = 0;
}

class DelayedModifier : public Modifier
{
    struct Modification { virtual void run(ActionPlayer&) = 0; }

    struct RemoveAction : public Modification
    {
        int id;

        Removal(int _id) : id(_id) {} 
        virtual void run(ActionPlayer &player) { player.removeAction(id); }
    }

    struct AddAction : public Modification
    {
        int id;
        std::function<void(Modifier&)>> action;

        AddAction(int _id, const std::function<void(Modifier&)> &_action) : id(_id), action(_action)  {}
        virtual void run(ActionPlayer &player) { player.addAction(id, action) };
    }

    ActionPlayer &player;
    std::vector<Modification> modifications;

public:
    DelayedModifier(ActionPlayer &_player) player(_player) {}
    virtual ~DelayedModifier() { run(); }

    virtual void addAction(int id, const std::function<void(Modifier&)> &action) { modifications.push_back(AddAction(id, action)); }
    virtual void removeAction(int id) { modifications.push_back(RemoveAction(id)); }

    void run()
    {
        for (auto &modification : modifications)
            modification.run(player);
        modifications.clear();
    }
};

所以你现在写:

ActionPlayer player;

{
    DelayedModifier modifier(player);

    modifier.addAction(...);
    modifier.addAction(...);
    modifier.run();
    actions.run(modifier);
}