Pull to refresh

Почему C++ не подходит для написания графических пользовательских интерфейсов

Reading time5 min
Views19K

Я люблю C++, но...



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

Однажды я столкнулся с развивающейся библиотекой GUI класса. С точки зрения C++, а точнее его классов, экземпляров и иерархий, этот язык представляется невероятно близким к концепции управления GUI, в частности таким элементам, как виджеты, окна классов и подокна. OO модели C++ и оконной системы тем не менее разные. C++ был задуман как «статический» язык с охватом лексем, статической проверкой типов и иерархий определенных во время компиляции. Окна и их объекты с другой стороны, по своей природе динамичны, они обычно живут за рамкам отдельной процедуры или блока, с помощью которой были созданы; иерархии виджетов в значительной степени определены расположением, видимостью и потоками событий. Основы графического пользовательского интерфейса, такие как динамические и геометрические иерархии окон и управления, протекания событий, не поддерживаются непосредственно синтаксисом С++ либо его семантикой. Таким образом, эти функции должны быть воспроизведены в коде C++ GUI. Это приводит к дублированию графического инструментария, или функциональности оконного менеджера, код «раздувается», мы вынуждены отказываться от многих «сильных» особенностей C++ (например, проверки типов во время компиляции). В статье приведено несколько простых примеров C++ / GUI «не стыковок».


Не создавайте конструкторы (или, по крайней мере, не используйте их)



Когда полученный класс переопределяет виртуальный метод родительского класса, надо иметь в виду, что переопределение не вступает в силу, пока конструктор базового класса выполняется. Это особенно раздражает, когда объекты запрашивают виджеты, которые реагируют на события GUI. Предположим, что класс Basic_Window был предназначен для создания ванильного черно-белого окна на экране:

class Basic_Window {
public:
Basic_Window(Rect rect) { gt_create_window(rect,visible,this); }
virtual void handle_create_event() { set_background(WHITE); }
};


Здесь gt_create_window() отвечает за низкоуровневый вызов основного графического инструментария (например, xvt_win_create()). Эта функция выделяет место под данные инструментария, уведомляет оконный менеджер, регистрирует этот объект, как получателя событий, и в примере выше, инициализирует графический вывод в окно на экране.
Предположим, мы хотим создать экземпляр Basic_Window, но с красным фоном. Обычно, чтобы изменить поведение класса, надо извлечь из него и переопределить соответствующие виртуальные методы. Мы пишем:

class RedWindow : public Basic_Window {
virtual void handle_create_event() { set_background(RED); }
public:
RedWindow(Rect rect) : Basic_Window(Rect rect) {}
};
RedWindow red_window(default_rect);


Но red_window появится белым, а не красным! Чтобы создать RedWindow, родительский объект должен быть создан первым. После завершения Basic_Window::Basic_Window(), виртуальные таблицы RedWindow вступают в силу, метод handle_create_event() становится неприменимым, и конструктор RedWindow() выполняется. Конструктор Basic_Window() регистрирует объект графического инструментария, который мгновенно начинает посылать события объекту (например, CREATE-событие). Конструктор Basic_Window() еще не закончен (это не гарантировано), поэтому переопределенный виртуальный метод еще не на месте. Таким образом, CREATE-событие будет обрабатываться Basic_Window::handle_create_event(). Виртуальные таблицы RedWindow класса будут созданы лишь тогда, когда базовый класс полностью построен, то есть, когда окно уже на экране. Изменение цвета окна на данном этапе приведет к досадной ошибке.

Есть простой обходной путь: запретить каждому конструктору регистрировать объект графического инструментария. Обработка событий будет построена таким образом, чтобы сдержать окончание инициализации до производных классов. Очень заманчиво рассматривать виджеты на экране, как «лицо» объекта GUI приложения в памяти. Как показывает приведенный выше пример, эта связь между экраном и C++ объектом реализовать не так просто: они рождаются отдельно.

Нет синтаксических средств переключения событий



Предположим, что библиотека классов включает в себя графический интерфейс класса PictWindow, который отображает фотографию в окно:

class PictWindow {
Picture picture;
public:
virtual void repaint() { gt_draw_pict(picture); }
...
};


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

class OvWindow : public PictWindow {
Rect rect;
virtual void repaint() { gt_draw_rect(rect); }
};


К сожалению, когда мы создадим экземпляр OvWindow, то увидим только прямоугольник в пустом окно, и никакого изображения. С того момента, как OvWindow::repaint() переопределяет PictWindow::repaint(), последняя функция не будет вызываться, когда окно должно быть отрисовано. Мы должны были реализовать OvWindow так:

class OvWindow : public PictWindow {
Rect rect;
virtual void repaint() { PictWindow::repaint(); gt_draw_rect(rect); }
public:
OvWindow(void) : PictWindow() {}
};


Конструктор OvWindow изложен, чтобы подчеркнуть, что метод OvWindow::repaint() должен быть отложен до суперкласса, как делают конструкторы. В самом деле, конструктор производного объекта с начала вызывает конструктор корреспондирующего объекта. repaint() следует отложить его родитель: методу в базовом классе, который его переопределяет.

В итоге: Плохая совместимость C++ / GUI



C++ был разработан как «статический» язык:
  • с отслеживанием лексем
  • проверкой статических типов
  • со статическими иерархиями классов
  • без «сборки мусора»
  • с системой сообщений без определенных иерархий времени компиляции


GUI объекты:
  • характерны динамические объекты, а зачастую и единственные в своем роде
  • как правило, живут далеко за рамками, в которых они были созданы
  • иерархии определяются в значительной степени потоками событий и их расположением, а не наследованием классов
  • иерархии строятся и разрушаются во время выполнения, зачастую в ответ на непредсказуемые действия пользователя


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

Выводы



Конечно, все эти «коряги» не являются фатальными. C++ является универсальным и мощным языком и, следовательно, способен выразить все возможные алгоритмы расчета. Поэтому, если приложение требует динамических функций, таких как те, что есть в Tcl / Tk, Scheme / Tk, Postscript и подобных; используя C++, Вы всегда можете сделать что-то по их примеру. С другой стороны, почему бы не использовать язык, где все эти черты присутствуют?
Tags:
Hubs:
Total votes 55: ↑32 and ↓23+9
Comments48

Articles