异常安全的 for 循环

Exception-safe for loop

考虑以下代码。

#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 是一个 setter: Item::set_enabled(bool),我想实现相同的ItemsContainer 的事情。这意味着如果启用失败,项目可以被禁用,但这里同样没有异常保证的规范。

好吧,这看起来更像是一个应用程序选择,而不是 API 选择。根据您的应用程序,您可能想要执行以下任一操作:

  1. 立即失败,让所有 Item 保持当前状态
  2. 在失败时将所有 Item 重置为之前的状态(事务类型保护)然后抛出
  3. 制作一个"best attempt"设置全部。

如果您认为您的用户可能需要不止一个,那么您可以轻松地将其设计到 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 return 值表示成功。或者,您可以 return 成功启用的项目数。如果失败的可能性很大,这就是您应该做的。如果失败的可能性非常非常小,或者如果失败表明是系统故障,那么你应该抛出。

如果你依赖的库没有给你异常情况下的行为保证,那么你就不能给依赖的行为保证。如果它给你基本的保证,你可以在一定程度上使用它,但可能会付出非常高的成本。

例如,在您的情况下,您希望保持所有项目都启用或 none 启用的不变性。所以你的选择是

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