简体   繁体   English

异常安全的循环

[英]Exception-safe for loop

Consider the following code. 考虑下面的代码。

#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;
};

In my situation, Item is implemented as shown (I cannot change it) and I am trying to implement ItemsContainer . 在我的情况下, Item的实现如图所示(我无法更改),并且我正在尝试实现ItemsContainer I don't exactly know how to deal with exceptions. 我不完全知道如何处理异常。

  1. In the proposed implementation, when enabling the container, we enable all the items even if notifying the observers of one of them throws, and in the end, we potentially throw an exception to notify the caller that there was a problem with (at least) one of the observers. 在建议的实现中,启用容器时,即使通知观察者其中一个抛出异常,我们也会启用所有项目,最后,我们有可能抛出异常以通知调用方(至少)存在问题观察者之一。 However the state of the container is modified. 但是,容器的状态已修改。 Is it counter-intuitive? 这是违反直觉的吗? Should I try to give the strong exception guarantee to ItemsContainer::enable by disabling the already enabled items in case of failure and keeping initial state unchanged? 我是否应该通过在发生故障的情况下禁用已启用的项目并保持初始状态不变来尝试为ItemsContainer::enable赋予强烈的异常保证?

  2. In ItemsContainer::enabled , are the assertions reasonable? ItemsContainer::enabled ,断言是否合理? Since I don't have the control on Item and no exception guarantee is documented, it could occur that the instructions m_enabled = true; 由于我没有对Item的控制,并且没有记录任何例外保证,因此可能会发生m_enabled = true;指令m_enabled = true; and notify_observers(); notify_observers(); are swapped. 被交换。 In this case, ItemsContainer::enable could break the internal state of the container. 在这种情况下, ItemsContainer::enable可能会破坏容器的内部状态。

It is not the first time I face such a situation. 我已经不是第一次遇到这种情况了。 Are there best practice rules? 有最佳实践规则吗?

NB: I reduced the code sample to the minimum, but in fact, Item::enable is a setter: Item::set_enabled(bool) and I want to implement the same thing for ItemsContainer . 注意: 我将代码示例减至最少,但实际上, Item::enable是一个setter: Item::set_enabled(bool) ,我想为ItemsContainer实现相同的功能。 This means that items can be disabled if enabling fails, but here again, without specification of the exception guarantee. 这意味着,如果启用失败,则可以禁用项目,但是在此情况下,无需指定异常保证。

If the library you depend on does not give you behavior guarantees in exceptional cases, then you cannot give dependent behavior guarantees. 如果您所依赖的库在特殊情况下没有给您行为保证,那么您就不能提供从属行为保证。 If it gives you the basic guarantee, you can work with that to a point, but potentially at very high cost. 如果它为您提供了基本保证,则可以在一定程度上使用该保证,但成本可能很高。

In your case, for example, you want to maintain the invariant that either all items are enable, or none are. 例如,在您的情况下,您想要保持不变,即所有项均已启用,或者都不启用。 So your options are 所以你的选择是

  1. If any enable function throws an exception, keep going. 如果任何使能函数引发异常,请继续进行。 This only works if you know the state of the item after an exception, but you said this isn't documented, so this option is out. 仅当您知道异常后项目的状态时,此方法才有效,但您说这没有记录,因此该选项已退出。 Even if it was documented, can you be sure that the observers are prepared to deal with this? 即使已将其记录在案,您是否可以确定观察员已准备好处理此问题? What does it mean if the first of many observers of a single item throws - do the other observers get called? 如果单个项目的多个观察者中的第一个抛出异常,那是什么意思?其他观察者是否被召唤? Does your entire application go into an invalid state if an item is enabled but the observers never knew? 如果启用了一项但观察者却不知道,则整个应用程序会进入无效状态吗?
  2. When an exception is thrown, go back through the enabled items and disable them again. 当引发异常时,请返回启用的项目并再次禁用它们。 But is this possible without throwing an exception again? 但这又有可能再次抛出异常吗? Do the observers need to be informed of this additional change? 是否需要将这些额外的更改告知观察者? And couldn't that in turn yield another exception? 难道这又不会产生另一个例外吗?
  3. Do something like Dennis's setEnabledWithTransaction . 做类似Dennis的setEnabledWithTransaction Is Item copyable? Item复制吗? What does it mean for the observers if an item is copied, but the copy then discarded, or the old object modified? 如果复制了一个项目,但是副本被丢弃,或者修改了旧对象,对观察者意味着什么?
  4. Maintain just the basic exception guarantee and clear the entire vector when an exception is thrown. 仅保留基本的异常保证,并在引发异常时清除整个向量。 This may seem radical, but if you can't find a way to make the above options work, this is the only way to maintain your class invariant that either all items are enabled or none are. 这看起来似乎是激进的,但是如果您找不到使上述选项起作用的方法,则这是保持类不变的唯一方法,即所有项都已启用或没有。 If you don't know the state of your items and you can't safely modify it, then you can only throw them away. 如果您不知道商品的状态并且无法安全地修改商品,则只能扔掉它们。
  5. Don't give any exception guarantee and leave the container in an inconsistent state. 不要提供任何例外保证,并让容器处于不一致状态。 This is of course a very bad option. 这当然是一个非常糟糕的选择。 Operations that don't at least give the basic exception guarantee have no place in a system that uses exceptions. 至少没有提供基本异常保证的操作在使用异常的系统中没有位置。

Well this seems more like an application choice than an API choice. 嗯,这似乎更像是应用程序的选择,而不是API的选择。 Depending on your application you may want to do any of: 根据您的应用程序,您可能需要执行以下任一操作:

  1. Fail immediately, leaving all Item s in their current state 立即失败,使所有Item保持当前状态
  2. Reset all Item s to previous state (transaction type protection) on failure then throw 失败时将所有Item重置为先前状态(事务类型保护),然后抛出
  3. Make a "best attempt" to set all. 尽一切可能。

If you think that your users may want more than one of those then you could easily design it into the API: 如果您认为用户可能需要多个用户,则可以轻松地将其设计到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;
}

Note that the bool return value indicated success. 请注意, bool返回值指示成功。 Alternatively you could return the number of successfully enabled items. 或者,您可以返回成功启用的项目数。 This is what you should do if the chance of a failure is non-trivial. 如果失败的机会微不足道,这就是您应该采取的措施。 If the chance of a failure is very very small, or if a failure indicates something that is a system failure then you should throw. 如果发生故障的机会非常小,或者如果发生故障表明系统出现故障,则应该抛出该故障。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM