Как обрабатывать сбой конструктора для RAII
Я знаком с преимуществами RAII, но недавно я споткнулся о проблему в коде, как это:
class Foo
{
public:
Foo()
{
DoSomething();
...
}
~Foo()
{
UndoSomething();
}
}
все в порядке, кроме этого кода в конструкторе бросил исключение, в результате чего UndoSomething()
так и не позвонили.
есть очевидные способы исправления этой конкретной проблемы, например wrap ...
в блоке try / catch, который затем вызывает UndoSomething()
, но a: это дублирующий код, А B: блоки try / catch-это запах кода, который я пытаюсь и избегайте использования методов RAII. И код, вероятно, станет хуже и более подвержен ошибкам, если задействовано несколько пар "сделать/отменить", и мы должны очистить половину пути.
мне интересно, есть лучший подход к этому - может быть отдельный объект принимает указатель на функцию и вызывает функцию, когда она, в свою очередь, происходит?
class Bar
{
FuncPtr f;
Bar() : f(NULL)
{
}
~Bar()
{
if (f != NULL)
f();
}
}
Я знаю, что не будет компилироваться, но он должен показать принцип. Фу тогда становится...
class Foo
{
Bar b;
Foo()
{
DoSomething();
b.f = UndoSomething;
...
}
}
Примечание. этот foo теперь не требует деструктора. Это звучит как больше проблем, чем это стоит, или это уже распространенный шаблон с чем-то полезным в boost для обработки тяжелого подъема для меня?
6 ответов
проблема в том, что ваш класс пытается сделать слишком много. Принцип RAII заключается в том, что он приобретает ресурс (либо в конструкторе, либо позже), и деструктор освобождает его; класс существует исключительно для управления этим ресурсом.
в вашем случае, ничего другого, кроме DoSomething()
и UndoSomething()
должен нести ответственность пользователь класса, а не сам класс.
как говорит Стив Джессоп в комментариях: если у вас есть несколько ресурсов для приобретения, то каждый должен управляться собственным объектом RAII; и может иметь смысл агрегировать их как члены данных другого класса, который создает каждый по очереди. Затем, если какое-либо приобретение не удастся, все ранее приобретенные ресурсы будут автоматически освобождены деструкторами отдельных членов класса.
(кроме того, помните правило трех; ваш класс должен либо предотвратить копирование, либо реализовать его каким-либо разумным способом, чтобы предотвратить несколько вызовов UndoSomething()
).
просто сделать DoSomething
/UndoSomething
в надлежащую ручку RAII:
struct SomethingHandle
{
SomethingHandle()
{
DoSomething();
// nothing else. Now the constructor is exception safe
}
SomethingHandle(SomethingHandle const&) = delete; // rule of three
~SomethingHandle()
{
UndoSomething();
}
}
class Foo
{
SomethingHandle something;
public:
Foo() : something() { // all for free
// rest of the code
}
}
Я бы решил это с помощью RAII тоже:
class Doer
{
Doer()
{ DoSomething(); }
~Doer()
{ UndoSomething(); }
};
class Foo
{
Doer doer;
public:
Foo()
{
...
}
};
исполнитель создается до запуска тела ctor и уничтожается либо при сбое деструктора через исключение, либо при обычном уничтожении объекта.
у вас слишком много в одном классе. Переместить DoSomething / UndoSomething в другой класс ("что-то") и иметь объект этого класса как часть класса Foo, таким образом:
class Foo
{
public:
Foo()
{
...
}
~Foo()
{
}
private:
class Something {
Something() { DoSomething(); }
~Something() { UndoSomething(); }
};
Something s;
}
теперь, DoSomething был вызван к моменту вызова конструктора Foo, и если конструктор Foo бросает, то UndoSomething получает правильный вызов.
try / catch is не запах кода вообще, он должен использоваться для обработки ошибок. В вашем случае это будет запах кода, потому что он не обрабатывает ошибку, а просто очищает. Для этого и существуют деструкторы.
(1) Если все в деструкторе должен вызываться, когда конструктор терпит неудачу, просто переместите его в частную функцию очистки, которая вызывается деструктором, и конструктором в случае сбоя. Кажется, это то, что ... ты уже сделал это. Хорошая работа.
(2) лучшая идея: если есть несколько пар do/undo, которые могут разрушаться отдельно, они должны быть завернуты в свой собственный маленький класс RAII, который делает это minitask и очищается после себя. Мне не нравится ваша текущая идея дать ему необязательную функцию указателя очистки, это просто запутывает. Очистка всегда должна быть сопряжена с инициализацией, это основная концепция RAII.
правила:
- если ваш класс вручную управляет созданием и удалением чего-либо, он делает слишком много.
- если ваш класс вручную написал copy-assignment/-construction, он, вероятно, управляет слишком много
- исключение из этого: класс, который имеет единственную цель управления ровно одной сущностью
примерами для третьего правила являются std::shared_ptr
, std::unique_ptr
, scope_guard
, std::vector<>
, std::list<>
, scoped_lock
и конечно Trasher
классом ниже.
дополнения.
вы можете зайти так далеко и написать что-то, чтобы взаимодействовать с вещами в стиле C:
#include <functional>
#include <iostream>
#include <stdexcept>
class Trasher {
public:
Trasher (std::function<void()> init, std::function<void()> deleter)
: deleter_(deleter)
{
init();
}
~Trasher ()
{
deleter_();
}
// non-copyable
Trasher& operator= (Trasher const&) = delete;
Trasher (Trasher const&) = delete;
private:
std::function<void()> deleter_;
};
class Foo {
public:
Foo ()
: meh_([](){std::cout << "hello!" << std::endl;},
[](){std::cout << "bye!" << std::endl;})
, moo_([](){std::cout << "be or not" << std::endl;},
[](){std::cout << "is the question" << std::endl;})
{
std::cout << "Fooborn." << std::endl;
throw std::runtime_error("oh oh");
}
~Foo() {
std::cout << "Foo in agony." << std::endl;
}
private:
Trasher meh_, moo_;
};
int main () {
try {
Foo foo;
} catch(std::exception &e) {
std::cerr << "error:" << e.what() << std::endl;
}
}
выход:
hello!
be or not
Fooborn.
is the question
bye!
error:oh oh
и ~Foo()
никогда не запускается, но ваша пара init/delete.
одна хорошая вещь: если ваша init-функция сама бросает, ваша функция удаления не будет вызываться, так как любое исключение, брошенное init-функцией, проходит прямо через Trasher()
и поэтому ~Trasher()
не выполнено.
Примечание: важно, что есть крайние try/catch
, в противном случае, размотка стека не требуется стандартом.