Как создать функцию с надежной гарантией исключения?

У меня есть функция, для которой я хотел бы иметь сильную гарантию исключения:

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 не должны выполняться, когда функция возвращается нормально.

Я также хотел бы, чтобы любое решение было нулевым - накладные расходы на счастливом пути; Мне очень нужно, чтобы счастливый путь был как можно быстрее.


person SomeProgrammer    schedule 13.04.2021    source источник
comment
Надежная гарантия исключений не всегда совместима с нулевыми накладными расходами. В этом случае, если вам действительно нужна надежная гарантия исключения, скопируйте исходный вектор, измените копию и swap после того, как будут выполнены опасные части. Это самое надежное решение.   -  person François Andrieux    schedule 13.04.2021
comment
Когда у вас есть блок catch, который предназначен для повторного создания, вы можете просто использовать throw; для повторного создания исключения. Это также единственный способ перебросить из универсального блока catch(...).   -  person François Andrieux    schedule 13.04.2021
comment
@Cory Kramer Да, это была ошибка, код отредактирован. Но предыдущий push_back мог быть успешным (например, push_back в векторе 3 не удался, но предыдущие 2 сработали), поэтому мне все еще нужно сделать несколько pop_back s.   -  person SomeProgrammer    schedule 13.04.2021
comment
@FrançoisAndrieux Я не могу позволить себе накладные расходы на счастливом пути ... Я не понимаю, почему в С++ нет механизма для предоставления более элегантного решения, чем то, которое я написал.   -  person SomeProgrammer    schedule 13.04.2021
comment
Каково именно ваше состояние восстановления? Вектор находиться в исходном состоянии? Если это так, почему бы не reserve сначала, и если это не удается, тогда вы знаете и можете бросить, иначе вызов push_back должен быть хорошим.   -  person NathanOliver    schedule 13.04.2021
comment
@NathanOliver, это разные векторы ...   -  person SomeProgrammer    schedule 13.04.2021
comment
у вас действительно есть четыре разных типа исключений?   -  person user253751    schedule 13.04.2021
comment
@SomeProgrammer Так и есть. :( ну, я думаю, это вышло.   -  person NathanOliver    schedule 13.04.2021
comment
@ user253751 Это скорее общий вопрос, чтобы я мог лучше понять обработку исключений, так что нет, это не настоящий код...   -  person SomeProgrammer    schedule 13.04.2021
comment
@SomeProgrammer Я не понимаю, почему в С++ нет механизма для предоставления более элегантного решения, чем то, которое я написал. Я не уверен, что разумно ожидать, что язык предложит -коробочное решение. Как вы ожидаете, что это сработает? Кажется, вы заметили, насколько сложна эта проблема из первых рук.   -  person François Andrieux    schedule 13.04.2021
comment
@FrançoisAndrieux Ну, они разработали RAII для обработки исключений, не так ли? Я думал, что есть (и, вероятно, есть) решение с использованием RAII...   -  person SomeProgrammer    schedule 13.04.2021
comment
@SomeProgrammer Отменить изменения, внесенные в функцию, нетривиально. В этом конкретном случае, поскольку вы только push_back, обратная функция проста. Вам просто нужно pop_back (независимо от изменения емкости). Но рассмотрим функцию, которая выполняет более трудные для обратного действия операции, такие как изменение существующих элементов. Для этой проблемы нет универсального решения, кроме копирования/обмена (что не всегда возможно, поэтому не является полностью общим). В противном случае вам придется реализовать гарантию исключения вручную в соответствии с вашим уникальным вариантом использования.   -  person François Andrieux    schedule 13.04.2021
comment
@ François Andrieux Да, вы правы. Но, учитывая обратную применяемую функцию, возможно ли легко обеспечить строгую гарантию исключения без дополнительных затрат на счастливом пути?   -  person SomeProgrammer    schedule 13.04.2021
comment
@SomeProgrammer Насколько я знаю, не существует решения для обработки ошибок с нулевой стоимостью. Даже отсутствие затрат на успешные реализации пути try/catch на самом деле не на 100% бесплатны, скорее они просто не намного медленнее и будут иметь ненулевую стоимость по сравнению с отсутствием обработки ошибок вообще. Кроме того, то, что представляет собой приемлемые накладные расходы, является вопросом мнения или, по крайней мере, вопросом требований конкретного варианта использования. Я бы попробовал scope_guard решение ниже и посмотреть, достаточно ли оно хорошо.   -  person François Andrieux    schedule 13.04.2021


Ответы (4)


Решение состоит в использовании защиты области действия.

См. этот ответ для примера их реализации; Я не буду здесь повторяться. С охранниками области ваш код будет выглядеть так:

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 — это макрос (см. ссылку выше). Вместо немедленного выполнения этого параметра он вызывает его выполнение, когда вы покидаете текущую область действия из-за исключения. Если вместо этого вы покинете область обычным способом, параметр будет проигнорирован. Если вы покинете прицел до того, как управление дойдет до охранника, оно также будет проигнорировано.

Учтите, что последний гард лишний, если после него (в том же прицеле) ничего не может бросить.

person HolyBlackCat    schedule 13.04.2021
comment
Хороший. Это именно то, что я искал. Знаете ли вы, если это нулевые накладные расходы на счастливом пути? - person SomeProgrammer; 13.04.2021
comment
@SomeProgrammer Возможно. Проверьте сгенерированную сборку, чтобы быть уверенным, с оптимизацией и/или без нее. - person HolyBlackCat; 13.04.2021
comment
Деструктор проверяет текущее количество исключений на счастливом пути. Маловероятно, что это будет оптимизировано, если только push_back не может бросить в первую очередь. - person François Andrieux; 13.04.2021

Проблема с простым возвратом заключается в том, что он не обязательно вернет вектор в исходное состояние. Если добавление элемента вызвало перераспределение любого из векторов, то итераторы/ссылки на элементы будут признаны недействительными, и эта недействительность не может быть отменена, что делает невозможным надежную гарантию исключения.

Безопасным, простым и общим решением является внесение изменений в копию. Копирование, конечно же, требует дополнительных затрат.

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 для защиты области действия является элегантным решением в тех случаях, когда возможен откат, например, если вы использовали другой контейнер, который не делает недействительным итераторы/ссылки, или если вас просто не волнует инвалидация, или если у вас есть инвариант класса, который предотвращает вызов функции, когда емкость заполнена.

Вы могли бы реально и с минимальными дополнительными затратами сначала проверить, все ли векторы имеют дополнительную емкость, и выбрать между копированием и откатом на основе проверки. Это позволяет вызывающему абоненту не оплачивать стоимость копирования, если он заранее резервирует достаточную емкость. Однако это далеко от элегантности.

person eerorika    schedule 13.04.2021
comment
Я не понимаю. Я просто pop_back, когда push_back удалось... - person SomeProgrammer; 13.04.2021
comment
@SomeProgrammer И если push_back вызвал перераспределение вектора, то он был перераспределен даже после извлечения перемещенного элемента. Таким образом, вектор не находится в том состоянии, в котором он находился до вызова функции. - person eerorika; 13.04.2021
comment
Ну да, технически вы правы, я думаю... но предположим, что у меня нет итераторов/ссылок на элементы. Можете ли вы найти более элегантное решение, чем мое, но при этом с нулевыми накладными расходами на счастливом пути? - person SomeProgrammer; 13.04.2021
comment
Если инвариантность емкости и аннулирование ссылки не являются проблемой, то простое reserveопределение требуемой емкости перед вставкой элементов было бы хорошим компромиссом, при условии, что конструкторы элементов также потенциально не выбрасывают. - person François Andrieux; 13.04.2021

Вы можете сделать это разными способами... Например, так:

#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 (или любым другим исключением) - вы не можете выполнить шаг отмены.

Также, возможно, в вашем случае имеет смысл использовать базовую гарантию? На практике вы часто можете иметь дело с этим на более высоком уровне - например. отключиться и сбросить все накопленное состояние, оставив клиенту возможность повторно подключиться и повторить попытку.

person C.M.    schedule 13.04.2021

Как насчет этого?

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
   }
};
person user253751    schedule 13.04.2021
comment
Что ж, ваше решение вряд ли более элегантно, чем мое... :) И это более общий вопрос, чтобы я мог элегантно писать строго безопасные функции для исключений, чтобы у меня не было конкретной проблемы с 4 векторами... - person SomeProgrammer; 13.04.2021