четверг, 22 января 2009 г.

C++ exception handlers

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

Среди этих правил есть: однозначное создание\инициализация объекта и его удаление\деинициализация. Кроме того, должна быть эффективная система обработки ошибок, позволяющая в любой момент прерывать исполнение и передавать работу контролееру за исключительными ситуациями: обработчику исключений. Теперь самое главное правило - любой объектный язык должен эффективно совмещать эти правила вместе. Правильно, я говорю про исключения в конструкторе.

Любой опытный разработчик знает о том, что в С++ нельзя бросать исключения в конструкторе и это приводит к ужасным последствиям, я постораюсь объяснить почему.

Среда исполнения имеет кучу различных объектов на пмяти, инструкции по их созданию и удалению, и два состояния у каждого объекта: инициализирован\не инициализирован. В случае успешной инициализации он попадает в общую кучу и будет обсолютно корректно уничтожен и в случае необходимости. Однако, объект который не смог завершить свою инициализацию может содержать часть инициализированных полей, деструктор для которых не будет вызван.

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

Конструктор, который заполняет память выделенную для его класса, условно, пробегается сверху вниз и заполняет содержимое новыми значениями. Эти значения могут быть ссылки на только-что выделенную динамическую память, которой класс хочет управлять в дальнейшем. Однако, для среды исполнения эта вновь выделенная память никак не связана с ее владельцем и в случае ошибки останется висеть в куче. Если пробежавшись до половины памяти класса он вызывает исключение и передает управление среде исполнения блок памяти считается целиком не инициализированным и на него не будет вызван дестркутор класса.

На первый взгляд кажется, что среда исполнения должна в любом случае вызывать деструктор на эту область памяти, которая была вполовину инициализирована конструктором. Но нет ответа на вопрос: что делать если деструктор вызовится на область памяти, которая не инициализирована? В этом случае деструктор класса может попытаться осободить\обратиться даже на не ицициализированный указатель памяти и это привидет к куда более серезеным последствиям чем утечка.

Как например в примере, приведенным ниже, если бы среда исполнения несмотря на искючение вызвала бы деструктор класса m (хочу обратить внимание, что она вызывает десткрукторы других, успешно инициализированных, классов) то переменная j содержала бы неопределенное значение и в деструкторе небылобы возможности проверить ее на валидность.

class My
{
  int i;
  int *k;
  int *j;

public:
  My ()
  {
    i = 0;
    k = new int[5];
    throw exception();
    j = new int[5];
  }
  ~My ()
  {
    fprint("%x\n",i);
    fprint("%x\n",k);
    fprint("%x\n",j);
    delete k;
    delete j;
  }
};

int main(int c, const char** a)
{
  try
  {
    int k = 0;
    {
       My m;
    } // в этом месте мог бы быть вызван деструктор класса m. Но так как память m была не инициализированна этого не происходит.
      // хочу обратить внимание, что если бы до класса My m стоял бы другой успешно инициализированный класс, его дестркуртор был бы вызван
  }catch(std::exception &e)
  {
    // solve it
  }
  return 0;
};


Другая, более сложная ситуация, когда класс содержит локальные переменные-классы. В этом случае если класс My2 бросает исключение при инициализации своих локальных перменных еще не войдя в тело конструктора.

class My2
{
  int i;
  MyNormal m;
  My m;

  My2() try
  {
    // body constructor
  } catch(std::exception &e)
  {
     // solve it
  }
};

В этом случае я часто встречаю решение данной проблеммы - игнорирование исключений. Разработчики вставляют конструкцию try/catch в тело конструктора и любые исключительные ситуации отправляют в системный журнал - это крайне не допустимо! В использовать журналирование - означает игнорировать ошибку и создавать класс не инициализированный, не пригодный для использования, что порождает ошибку второго уровня. Это в свою очередь опять таки не допустимо.



Вывод:
Из всего вышесказанного я делаю вывод, что в С++ нельзя инициализировать класс в его конструкторе, нельзя использовать локальные переменные состоящие не из простых типов и указателей (тоже простой тип), так как это в ряде случаев приводит к серезным ошибкам. Следовательно - С++ не объектный язык :)

Для того чтобы писать на С++ вам необходимо проработать с языком не меньше 5 лет и узнать обо всех тонкостях, выработать и научится хорошему стилю программирования и исключать из своих разработок код привиденный выше.

3 коммент.:

  1. АнонимныйApr 12, 2009 11:52 PM
    Если кто-нибудь еще вздумает прочитать этот пост не смейте верить!!! тут слишком много неправильного. Читайте лучше "классические труды" по С++ там все эти "проблемы" разобраны и даны ПРАВИЛЬНЫЕ решения.
    ОтветитьУдалить
  2. АнонимныйJun 8, 2009 04:43 AM
    Поддерживаю, ерунда здесь написана полная.
    ОтветитьУдалить
  3. АнонимныйJun 23, 2009 07:26 PM
    Аффтару: см. напр. идиому "захват ресурса есть инициализация" и поменьше бреда в дальнейшем...
    ОтветитьУдалить