异常安全的 for 循环

Exception-safe for loop

本文关键字:循环 for 安全 异常      更新时间:2023-10-16

请考虑以下代码。

#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可能会破坏容器的内部状态。

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

注意:我将代码示例减少到最低限度,但实际上,Item::enable是一个二传手:Item::set_enabled(bool)和我想为ItemsContainer实现同样的事情。这意味着如果启用失败,则可以禁用项目,但同样,没有指定例外保证。

如果您依赖的库在特殊情况下不为您提供行为保证,则无法提供依赖行为保证。如果它为您提供了基本保证,您可以在一定程度上使用它,但成本可能非常高。

例如,在您

的情况下,您希望保持不变性,即启用所有项目或不启用任何项目。所以你的选择是

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

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

  1. 立即失败,使所有Item都处于当前状态
  2. 失败时将所有Item重置为以前的状态(事务类型保护),然后抛出
  3. 做出"最佳尝试"来设置所有内容。

如果您认为您的用户可能需要多个这些,那么您可以轻松地将其设计到 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返回值表示成功。或者,您可以返回成功启用的项目数。如果失败的机会不平凡,您应该这样做。如果发生故障的可能性非常小,或者如果故障表明某些东西是系统故障,那么您应该抛出。