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