Заметки из мира IT

В стиле минимализм

ZeroMQ является современной технологией обмена асинхронными сообщениями между высоко нагруженными системами. В первую очередь ZeroMQ интересен тем, что сетевое взаимодействие происходит через новый уровень...
http://dynamic.versusit.ru/Files/2013/4b9b093b-93ec-411d-ab4c-bfbd32f013b7.png

ZeroMQ является современной технологией обмена асинхронными сообщениями между высоко нагруженными системами. В первую очередь ZeroMQ интересен тем, что сетевое взаимодействие происходит через новый уровень сетевого стека, который может использоваться в качестве транспорта TCP, PC, PGM и тд. Система очень быстрая и надежная.

Я использую С++ в своей работе крайне широко. Это пожалуй мой главный инструмент. Естественно, что когда я начал проект ZeroMQ в далеком 2007 году в качестве языка я выбрал С++. Причины были следующие:

1. Библиотека шаблонов STL является частью языка. На Си мне бы пришлось завязать проект на сторонние библиотеки с алгоритмами написанные на манер 1970 годов

2. С++ вводит некий регламент для стиля написания кода. К примеру параметр «this» не позволяет передать указатель на объект. Тоже касается видимости переменных и ряда других полезных вещей.

3. Пункт касается так же указателей, но уже в другом ключе:

Создание виртуальных функций в С очень сложно и при реализации их для каждого класса приносит множество сложностей и неимоверно усложняет код.

4. Последнее: всем удобны деструкторы, которые можно вызывать в конце каждого блока кода.

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

Тут нужно заметить, что ZeroMQ был запланирован как часть отказо-устойчивой архитектуры. Он никогда не должен был работать с ошибками или иметь непредвиденное поведение. Поэтому перехват ошибок был крайне важен. А сам их перехват должен был быть очень качественным.




Перехват ошибок в С++ весьма продуман. Он позволяет создавать приложения, которые не упадут просто так — достаточно обернуть главную функцию main в блоке try/catch и все ошибки будут пойманы в одном месте.

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

На С поймать и обработать ошибку можно тот час же когда она случилась. Это позволяет писать подобный код:

int rc = fx ();
if (rc != 0)
   handle_error ();

На С++ когда ошибка случается нам необходимо перепрыгнуть к обработчику исключений:

int rc = fx ();
if (rc != 0)
   throw std::exception ();

Проблема в том, что мы не можем точно узнать где и почему произошло исключение. Можно конечно поместить обработчик в ту же функцию, но это не очень удобно  и ухудшает читабельность:

try {
   ...
   int rc = fx ();
   if (rc != 0)
       throw std::exception ("Error!");
   ...
catch (std::exception &e) {
   handle_exception ();
}

Так же весьма сложно понять когда в функции могли произойти две различные ошибки.

class exception1 {};
class exception2 {};

try {
   ...
   if (condition1)
       throw my_exception1 ();
   ...
   if (condition2)
       throw my_exception2 ();
   ...
}
catch (my_exception1 &e) {
   handle_exception1 ();
}
catch (my_exception2 &e) {
   handle_exception2 ();
}

А вот как это будет в C:

...
if (condition1)
  handle_exception1 ();
...
if (condition2)
  handle_exception2 ();

Это во первых более читабельно, а во вторых немного более производительно. Но это не самое главное. Может возникнуть ситуация, когда исключение не будет обработано в функции в которой оно произошло. Тогда обработка исключения может случиться в другом месте. И отлов подобных сюрпризов может превратится в настоящий ад.

Когда вы исправляете отдельный баг, вы обнаруживаете, что подобная ошибка встречается в коде во многих местах. А добавление нового кода будет приводить к появлению новых ошибок, которые еще не так хорошо отлавливаются. В конечном счете это порождает новые баги.

Если вы не хотите таких неприятных моментов как «непредвиденное поведение программы» вам придется создавать индивидуальные исключения во всех критичных местах. Но тогда таким образом придется обработать все куски кода.

Как показывает практика, исключения могут быть полезны для решения стандартных кейсов, но для нестандартных случаев они могут только вредить. В конечном итоге я пришел к тому, что выбрал за основной язык С++, но решил выбросить из него встроенный механизм исключений. Именно так сейчас реализован ZeroMQ и Crossroads I/O.

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

class foo
{
public:
    foo ();
    int init ();
    ...
};

Когда вы создаете экземпляр класса вызывается конструктор (и он не падает), но когда вы вызываете функцию инициализации, вот тут все может рухнуть

В С решение проблемы будет выглядеть так:

struct foo
{
    ...
};

int foo_init (struct foo *self);

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

Если обработка следующего кода не удалась, потребуется две отдельных функции для обработки случившегося:

class foo
{
public:

    ...

    int term ();

    ~foo ();
};

Теперь вернемся к проблеме инициализации. Некорректно завершенные процессы тоже нужно как-то обработать.

class foo
{
public:

    foo () : state (semi_initialised)
    {
        ...
    }

    int init ()
    {
       if (state != semi_initialised)
           handle_state_error ();
       ...
       state = intitialised;
    }
    int term ()

    {
        if (state != initialised)
            handle_state_error ();
        ...
        state = semi_terminated;
    }

    ~foo ()
    {
        if (state != semi_terminated)
            handle_state_error ();
        ...
    }

    int bar ()
    {
        if (state != initialised)
            handle_state_error ();
        ...
    }
};

Сравните пример выше с примером на чистом С. Для памяти и объекта есть только два состояния: неинициализированное и инициализированное:

struct foo
{
    ...
};

int foo_init ()

{
    ...
}

int foo_term ()

{
    ...
}

int foo_bar ()

{
    ...
}

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

class foo : public bar
{
public:
    foo () : bar () {}
    ...
};

Итого у нас будет мешанина из неинициализированных, полуинициализированных, полувыполенных сущностей. Это неминуемо породит огромное количество ошибок, которые будут проявлять себя через раз.

Как итог я могу сказать, что модель ООП не являются моделью с полностью предсказуемым поведением. И это является верным не только для С++, но и для любого другого языка в котором есть конструкторы и деструкторы. Однако, ООП языки по прежнему хороши там, где важна быстрая скорость разработки нежели тщательный контроль за выполнением кода. Поэтому системным языком программирования был и есть С.

admin

Яндекс.Метрика