循环异常安全

时间:2015-01-13 17:35:26

标签: c++ foreach exception-handling

请考虑以下代码。

#include <cassert>
#include <stdexcept>
#include <vector>

struct Item {
    Item() : m_enabled(false) {}

    void enable()
    {
        m_enabled = true;
        notify_observers(); // can throw
    }
    bool enabled() const {return m_enabled;}

private:
    bool m_enabled;
};

struct ItemsContainer {
    ItemsContainer() : m_enabled(false) {}

    void enable()
    {
        bool error = false;
        for(Item & item : m_items) {
            try {
                item.enable();
            } catch(...) {
                error = true;
            }
        }
        m_enabled = true;
        if(error) throw std::runtime_error("Failed to enable all items.");
    }
    bool enabled() const
    {
        for(Item const & item : m_items) assert(item.enabled() == m_enabled);
        return m_enabled;
    }

private:
    std::vector<Item> m_items;
    bool m_enabled;
};

在我的情况下,Item如图所示实现(我无法更改),我正在尝试实施ItemsContainer。我并不完全知道如何处理异常。

  1. 在提议的实现中,当启用容器时,即使通知观察者其中一个引发,我们也会启用所有项目,最后,我们可能会抛出异常以通知调用者有一个(至少)一名观察员的问题。但是,容器的状态被修改。这是违反直觉的吗?我是否应该尝试通过在发生故障时禁用已启用的项目并保持初始状态不变来为ItemsContainer::enable提供强大的异常保证?

  2. ItemsContainer::enabled中,断言合理吗?由于我没有对Item进行控制,并且未记录任何异常保证,因此可能会交换说明m_enabled = true;notify_observers();。在这种情况下,ItemsContainer::enable可能会破坏容器的内部状态。

  3. 这不是我第一次遇到这种情况。 是否有最佳实践规则?

    注意:我将代码示例减少到最小,但事实上,Item::enable是一个setter:Item::set_enabled(bool),我想为ItemsContainer实现相同的功能。这意味着如果启用失败,则可以禁用项目,但在此处再次,不指定异常保证。

2 个答案:

答案 0 :(得分:2)

如果你所依赖的图书馆在特殊情况下没有给你行为保证,那么你就不能给予依赖行为保证。如果它为您提供了基本保证,您可以将其用于某一点,但可能成本非常高。

在您的情况下,例如,您希望维护所有项都启用或不启用的不变量。所以你的选择是

  1. 如果任何启用函数抛出异常,请继续。这只有在您知道异常后项目的状态时才有效,但是您说这没有记录,所以这个选项已经出来了。即使记录在案,您能否确定观察者已准备好应对此问题?如果单个项目的许多观察者中的第一个投掷 - 这是什么意思 - 其他观察者会被调用吗?如果项目已启用但观察者从未知道,您的整个应用程序是否会进入无效状态?
  2. 抛出异常时,请返回已启用的项目并再次禁用它们。但这可能不会再次抛出异常吗?观察员是否需要了解这一额外变化?而这又不能产生另一个例外吗?
  3. 做一些像丹尼斯的setEnabledWithTransactionItem是否可以复制?如果复制了一个项目,然后丢弃了该副本,或者修改了旧对象,对观察者意味着什么?
  4. 保持基本异常保证并在抛出异常时清除整个向量。这可能看起来很激进,但是如果你找不到使上述选项有效的方法,这是维持你的类不变的唯一方法,即所有项都被启用或者没有。如果您不知道物品的状态而无法安全地修改它,那么您只能扔掉它们。
  5. 不提供任何例外保证并使容器处于不一致状态。这当然是一个非常糟糕的选择。至少不提供基本异常保证的操作在使用异常的系统中没有位置。

答案 1 :(得分:1)

这似乎更像是应用程序选择而不是API选择。根据您的应用程序,您可能希望执行以下任何操作:

  1. 立即失败,让所有Item处于当前状态
  2. 在失败时将所有Item重置为先前的状态(事务类型保护),然后抛出
  3. 做出最好的尝试&#34;设置所有。
  4. 如果您认为您的用户可能需要多个用户,那么您可以轻松地将其设计到API中:

    bool setEnabled(bool enable, bool failFast = false)
    {
        bool error = false;
        for(Item & item : m_items) {
            try {
                enable ? item.enable() : item.disable();
            } catch(...) {
                error = true;
                if(failFast)
                     throw std::runtime_error("Failed to enable all items.");
            }
        }
        m_enabled = true;
        return error;
    }
    
    bool setEnabledWithTransaction(bool enable)
    {
        bool error = false;
        vector<Item> clone(m_items);
        for(Item & item : m_items) {
            try {
                enable ? item.enable() : item.disable();
            } catch(...) {
               // use the saved state (clone) to revert to pre-operation state.
            }
        }
        m_enabled = true;
        return error;
    }
    

    请注意,bool返回值表示成功。或者,您可以返回成功启用的项目数。如果失败的可能性非常小,那么您应该这样做。如果失败的可能性非常小,或者如果失败表明系统出现故障,那么你应该扔掉。