Как справиться с ошибкой конструктора для RAII
Я знаком с преимуществами RAII, но я недавно опробовал проблему в коде следующим образом:
class Foo
{
public:
Foo()
{
DoSomething();
...
}
~Foo()
{
UndoSomething();
}
}
Все отлично, за исключением того, что код в секции конструктора ...
выдал исключение, в результате чего UndoSomething()
никогда не вызывался.
Есть очевидные способы устранения этой конкретной проблемы, например wrap ...
в блоке try/catch, который затем вызывает UndoSomething()
, но: этот дублирующий код и b: try/catch блоки - это запах кода, который Я стараюсь избегать использования методов RAII. И, скорее всего, код будет хуже и более подвержен ошибкам, если задействованы несколько пар Do/Undo, и мы должны очистить половину пути.
Мне интересно, есть ли лучший способ сделать это - может быть, отдельный объект принимает указатель на функцию и вызывает функцию, когда она, в свою очередь, разрушается?
class Bar
{
FuncPtr f;
Bar() : f(NULL)
{
}
~Bar()
{
if (f != NULL)
f();
}
}
Я знаю, что не будет компилироваться, но должен показать принцип. Затем Foo становится...
class Foo
{
Bar b;
Foo()
{
DoSomething();
b.f = UndoSomething;
...
}
}
Обратите внимание, что foo теперь не требует деструктора. Это звучит как больше неприятностей, чем стоит, или это уже общий шаблон с чем-то полезным в усилии для обработки тяжелой работы для меня?
Ответы
Ответ 1
Проблема в том, что ваш класс пытается сделать слишком много. Принцип RAII заключается в том, что он приобретает ресурс (либо в конструкторе, либо позже), и деструктор освобождает его; класс существует исключительно для управления этим ресурсом.
В вашем случае все, кроме DoSomething()
и UndoSomething()
, должно нести ответственность за класс, а не за класс.
Как говорит Стив Джессоп в комментариях: если у вас есть несколько ресурсов для приобретения, то каждый из них должен управляться своим собственным объектом RAII; и может иметь смысл объединить их в качестве членов данных другого класса, который строит каждый по очереди. Тогда, если какое-либо приобретение не удастся, все ранее приобретенные ресурсы будут автоматически выпущены деструкторами отдельных членов класса.
(Кроме того, помните Правило трех, ваш класс должен либо предотвратить копирование, либо реализовать его каким-то разумным способом, чтобы предотвратить несколько вызовов UndoSomething()
).
Ответ 2
Просто вставьте 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
}
}
Ответ 3
Я бы решил это использовать RAII тоже:
class Doer
{
Doer()
{ DoSomething(); }
~Doer()
{ UndoSomething(); }
};
class Foo
{
Doer doer;
public:
Foo()
{
...
}
};
Создатель создается до того, как тело ctor запустится и будет уничтожено либо, когда деструктор завершится с ошибкой через исключение или когда объект будет уничтожен нормально.
Ответ 4
У вас слишком много в вашем классе. Переместите DoSomething/UndoSomething в другой класс ( "Что-то" ), и у вас есть объект этого класса как часть класса Foo, таким образом:
class Foo
{
public:
Foo()
{
...
}
~Foo()
{
}
private:
class Something {
Something() { DoSomething(); }
~Something() { UndoSomething(); }
};
Something s;
}
Теперь DoSomething вызывается к тому моменту, когда вызывается конструктор Foo, и если вы создаете конструктор Foo, то корректно вызывается UndoSomething.
Ответ 5
try/catch не является запахом кода вообще, он должен использоваться для обработки ошибок. В вашем случае, однако, это будет запах кода, потому что он не обрабатывает ошибку, просто очищает. Для чего предназначены деструкторы.
(1) Если все вызовы в деструкторе вызываются при сбое конструктора, просто переместите его в функцию частной очистки, которая вызывается деструктором, а конструктор - в случае сбоя. Кажется, это то, что вы уже сделали. Хорошая работа.
(2) Лучшая идея: если несколько пар do/undo, которые могут разрушить отдельно, их следует обернуть в свой собственный класс RAII, который делает его мини-маской и очищается после себя. Мне не нравится ваша нынешняя идея предоставить ему необязательную функцию указателя очистки, которая просто запутывает. Очистка всегда должна быть сопряжена с инициализацией, что основная концепция RAII.
Ответ 6
Правила большого пальца:
- Если ваш класс вручную управляет созданием и удалением чего-либо, он делает слишком много.
- Если ваш класс имеет вручную написанное копирование/создание, возможно, слишком много работает
- Исключение из этого: класс, который имеет единственную цель управлять точно одним объектом
Примеры для третьего правила: 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, проходит прямо через Trasher()
, и поэтому ~Trasher()
не будет выполняется.
Примечание. Важно, что есть внешний try/catch
, в противном случае стандартное отклонение стека не требуется.