【问题标题】:How do I design a function with a strong exception guarantee?如何设计具有强异常保证的函数?
【发布时间】:2021-04-13 19:19:47
【问题描述】:

我有一个功能,我希望有强大的异常保证:

class X {
   /* Fields and stuff */
   void some_function() {
       vector1.push_back(/*...*/); // May Throw
       vector2.push_back(/*...*/); // May Throw
       vector3.push_back(/*...*/); // May Throw
       vector4.push_back(/*...*/); // May Throw
   }
};

我能想到使其具有强大的异常保证的唯一方法是:

class X {
   /* Fields and stuff */
   void some_function() {
       try { vector1.push_back(/*...*/);};
       catch (Vector1PushBackException) {
            throw Vector1PushBackException;
       }
       try { vector2.push_back(/*...*/);};
       catch (Vector2PushBackException) {
            vector1.pop_back();
            throw Vector2PushBackException;
       }
       try { vector3.push_back(/*...*/);};
       catch (Vector3PushBackException) {
            vector1.pop_back();
            vector2.pop_back();
            throw Vector3PushBackException;
       }
       try { vector4.push_back(/*...*/);};
       catch (Vector4PushBackException) {
            vector1.pop_back();
            vector2.pop_back();
            vector3.pop_back();
            throw Vector4PushBackException;
       }
   }
};


但是,这真的很丑而且容易出错!!有没有比我上面提到的更好的解决方案?我可以听到有人告诉我需要使用 RAII,但我无法弄清楚如何使用,因为函数正常返回时不能执行 pop_back 操作。

我还希望任何解决方案都是零 - 快乐路径上的开销;我真的需要尽可能快的快乐之路。

【问题讨论】:

  • 强异常保证并不总是与零开销兼容。在这种情况下,如果您确实需要强异常保证,请复制原始向量,修改副本和swap,一旦风险部分完成。这是最强大的解决方案。
  • 当您有一个打算重新抛出的catch 块时,您可以使用throw; 重新抛出异常。这也是从通用 catch(...) 块重新抛出的唯一方法。
  • @Cory Kramer 是的,这是一个错误,代码已编辑。但是之前的 push_back 可能已经成功了(例如 vector3 push_back 失败但之前的 2 有效)所以我仍然需要做一些 pop_back 。
  • 您的恢复状态到底是什么?向量处于其原始状态?如果是这样,为什么不先reserve,如果失败了,那么你知道并且可以抛出,否则对push_back的调用应该是好的。
  • 你真的有四种不同的异常类型吗?

标签: c++ raii exception-safety


【解决方案1】:

解决方法是使用scope guards

请参阅this answer 以获取它们的示例实现;我不打算在这里重复。使用范围保护,您的代码将如下所示:

vector1.push_back(/*...*/);
FINALLY_ON_THROW( vector1.pop_back(); )
vector2.push_back(/*...*/);
FINALLY_ON_THROW( vector2.pop_back(); )
vector3.push_back(/*...*/);
FINALLY_ON_THROW( vector3.pop_back(); )
vector4.push_back(/*...*/);
FINALLY_ON_THROW( vector4.pop_back(); )

这里,FINALLY_ON_THROW 是一个宏(参见上面的链接)。它不是立即执行它的参数,而是在您因为异常而离开当前范围时执行它。如果您以正常方式离开范围,则忽略该参数。如果您在控制权首先到达守卫之前离开范围,它也会被忽略。

请注意,如果最后一个守卫(在同一范围内)后面没有任何东西可以抛出,那么最后一个守卫是多余的。

【讨论】:

  • 不错。这正是我一直在寻找的。你知道这在幸福的道路上是否是零开销吗?
  • @SomeProgrammer 也许吧。检查生成的程序集以确保有和/或没有优化。
  • 析构函数检查快乐路径上的当前异常计数。除非push_back 不能放在首位,否则这不太可能被优化。
【解决方案2】:

简单地弹回的一个问题是它不一定会将向量恢复到原始状态。如果添加元素导致任何向量重新分配,则对元素的迭代器/引用将失效,并且该失效无法回滚,从而无法保证强异常。

一种安全、简单且通用的解决方案是对副本进行修改。复制当然需要额外的费用。

void some_function() {
    auto copy = *this;
    copy.vector1.push_back(/*...*/); // May Throw
    copy.vector2.push_back(/*...*/); // May Throw
    copy.vector3.push_back(/*...*/); // May Throw
    copy.vector4.push_back(/*...*/); // May Throw
    *this = std::move(copy);
}

HolyBlackCat's 范围保护建议是一个优雅的解决方案,在可能回滚的情况下,例如如果您使用了另一个不会使迭代器/引用无效的容器,或者您根本不关心失效,或者如果您有防止函数在容量已满时被调用的类不变量。

您可以以边际额外成本可行地首先检查所有向量是否具有额外容量,然后根据检查在复制和回滚之间进行选择。这允许调用者在事先保留足够容量的情况下不支付复制成本。然而,这确实偏离了优雅。

【讨论】:

  • 我不明白。当 push_back 成功时我只是 pop_back...
  • @SomeProgrammer 如果 push_back 导致向量重新分配,那么即使在弹出推送的元素之后它也被重新分配。因此,向量不在调用函数之前的状态。
  • 嗯,是的,从技术上讲,我猜你是对的......但假设我没有任何迭代器/对元素的引用。你能找到一个比我更优雅的解决方案,同时在快乐的道路上仍然是零开销吗?
  • 如果容量不变性和引用失效不是问题,那么在插入元素之前简单地reserveing 所需的容量将是一个很好的折衷方案,假设元素构造函数也不会抛出。跨度>
【解决方案3】:

您可以通过多种方式做到这一点...例如:

#include <vector>
#include <type_traits>
#include <exception>


template<class F>
struct on_fail
{
    F   f_;
    int count_{ std::uncaught_exceptions() };

    ~on_fail()
    {
        // C++20 is here and still no easy way to tell "unwinding" and "leaving scope" apart
        if (std::uncaught_exceptions() > count_) f_();
    }
};

template<class F> on_fail(F) -> on_fail<F>;


auto emplace_back_x(auto& v, auto&& x)
{
    v.emplace_back(std::forward<decltype(x)>(x));
    return on_fail{[&v]{ v.pop_back(); }};
}


int bar();


template<class F>
struct inplacer
{
    F f_;
    operator std::invoke_result_t<F&>() { return f_(); }
};

template<class F> inplacer(F) -> inplacer<F>;


void foo()
{
    std::vector<int> v1, v2, v3;
    auto rollback1 = emplace_back_x(v1, 1);
    auto rollback2 = emplace_back_x(v2, inplacer{ bar });
    auto rollback3 = emplace_back_x(v3, inplacer{ []{ return bar() + 1; } });
}

请注意,您的示例不正确:如果 push_back() 失败并出现 std::bad_alloc(或任何其他异常) - 您无法执行撤消步骤。

另外,在您的情况下,使用基本保证是否有意义?在实践中,您通常可以在更高的层次上处理它——例如断开连接并丢弃整个累积状态,让客户端重新连接并重复尝试。

【讨论】:

    【解决方案4】:

    这个怎么样?

    class X {
       /* Fields and stuff */
       void some_function() {
           vector1.push_back(/*...*/); // May Throw
           try {
               vector2.push_back(/*...*/); // May Throw
               try {
                   vector3.push_back(/*...*/); // May Throw
                   try {
                       vector4.push_back(/*...*/); // May Throw
                   } catch(...) {
                       vector3.pop_back();
                       throw;
                   }
               } catch(...) {
                   vector2.pop_back();
                   throw;
               }
           } catch(...) {
               vector1.pop_back();
               throw;
           }
       }
    };
    

    但是……你真的需要 4 个不同的向量吗?

    class X {
       /* Fields and stuff */
       void some_function() {
           vector1234.push_back(std::make_tuple(/*...*/, /*...*/, /*...*/, /*...*/)); // May Throw
       }
    };
    

    【讨论】:

    • 好吧,你的解决方案几乎没有我的优雅...... :) 这是一个更普遍的问题,所以我可以优雅地编写强异常安全函数,所以我没有具体的4个向量的问题...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-08
    • 2021-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多