Подтвердить что ты не робот

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

4b9b3361

Ответ 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, в противном случае стандартное отклонение стека не требуется.