【问题标题】:Exception-safe for loop异常安全的 for 循环
【发布时间】:2015-01-14 18:31:32
【问题描述】:

考虑下面的代码。

#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 实现相同的东西。这意味着如果启用失败,可以禁用项目,但这里再次说明,没有指定异常保证。

【问题讨论】:

    标签: c++ foreach exception-handling


    【解决方案1】:

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

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

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

    【讨论】:

    • 感谢您列出不同的选项,这表明问题并不那么容易,必须逐案确定解决方案。
    【解决方案2】:

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

    1. 立即失败,使所有Items 保持当前状态
    2. 在失败时将所有Items 重置为之前的状态(事务类型保护)然后抛出
    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 返回值表示成功。或者,您可以返回成功启用的项目数。如果失败的可能性不小,这就是你应该做的。如果失败的可能性非常小,或者如果失败表明系统故障,那么你应该抛出。

    【讨论】:

    • 感谢您提及交易,这是我开始采用的解决方案。正如您所说,做出的选择实际上取决于应用程序,这解释了为什么我们找不到独特的通用解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-19
    • 2014-07-01
    • 2013-07-30
    • 1970-01-01
    相关资源
    最近更新 更多