Показаны сообщения с ярлыком ООП. Показать все сообщения
Показаны сообщения с ярлыком ООП. Показать все сообщения

четверг, 19 июля 2012 г.

C++. Методы класса. Интересный вариант вызова

Началась эта маленькая история с того, что один мой молодой коллега, Алексей Заровный, прислал мне на посмотреть вот такую ссылку - http://ideone.com/gyMg9. По этой ссылке размещён очень короткий, но весьма интересный пример на языке C++. Позволю себе привести ниже его копию.

#include <iostream>
class Foo
{
public:
  void foo()
  {
    std::cout << "hello world" << std::endl;
  }
};
 
int main()
{
  Foo& foo = *(Foo*)NULL;
  foo.foo();
}

Наверное, не каждый из читателей сразу поймёт, что несмотря на необычный способ вызова метода, код, тем не менее совершенно трезвый и рабочий, хотя, конечно, вряд-ли кто-нибудь напишет это в реальном проекте. Однако для демонстрации понимания основ ООП пример очень достойный.

Признаюсь сразу, что мне понадобилась подсказка. Мой мозг программиста с более чем двадцатилетним стажем, по самое не хочу загруженный различными стереотипами кода, отказался принять представленную конструкцию даже после того, как я убедился, что это компилируется и работает! Однако, мне на радость, есть у меня один добрый такой коллега, который относительно недавно пополнил армию программистов, оставив не менее достойные ряды армии электронщиков. Зовут этого молодого гения Антон Березин :) Именно он подлил в мои заржавленные мозги капельку масла :) И тогда сразу все стало на свои места! Еще бы! Сколько раз я объяснял ЭТО своим ученикам при объяснении принципов реализациии ООП :)

Для тех, кто еще не понял в чем дело я приведу упрощенный вариант тела функции main().

int main()
{
  Foo *foo = (Foo*)0;
  foo->foo();
}

Есть ли здесь фокус или все совершенно справедливо? Для тех, кто по прежнему не понимает как это может работать приведу пояснения.

Давайте вспомним основы внутренней организации ООП. Если у нас есть некий класс с полями и методами, то что из себя представляет объект этого класса? Для того, чтобы быть понятнее приведу более конкретный пример.

class A
{
  public:
    void m();

private:
  int d;
}

A *a = new A();
a->m();

Итак, что такое a с точки зрения внутренностей языка C++? Наверное, на этот вопрос все ответят правильно. С точки зрения внутреннего устройства, а, это указатель на некую область памяти, где лежат данные класса A. Обратите внимание на очень важную часть ответа - "данные класса".

Конечно же! Экземпляр класса это не более чем распределённые в памяти нестатические данные класса.

Методы класса лежат в памяти отдельно, и в единственном экземпляре для всех объектов класса. Чтобы различать экземпляры данных, каждый метод класса имеет неявный параметр this, через который, в реальности, передается указатель на тот экземпляр данных, к которому относится текущий вызов метода. Таким образом, запись

  a->m();

семантически соответствует некой условно-функциональной записи A::m(a), т.е. вызывается метод m() класса A::, которому передается экземпляр данных класса A по указателю a (простите мне синтаксические вольности :) ).

Таким образом, запись ((Foo *)0)->foo(), соответствующая приведённому примеру означает, что был вызван метод foo() класса Foo:: и в него был передан нулевой указатель на данные. Но, так как метод foo() в своей реализации не использует каких-либо данных класса Foo, то и проблем с исполнением этого метода никаких нет.

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

В завершении добавлю пару вариаций на тему того, как может выглядеть функция main() в более изощрённом варианте обсуждаемого примера, предложенных моим коллегой по цеху программистов, Игорем Богомоловым.

int main()
{
  Foo& foo = *(Foo*)NULL;
  void(Foo::*f)(void) = &Foo::foo;
  (foo.*f)();
}
int main()
{
  Foo *foo = (Foo*)0;
  void(Foo::*f)(void) = &Foo::foo;
  (foo->*f)();
}

понедельник, 28 ноября 2011 г.

Немного об ООП в контексте ObjectiveC, Qt и C++




Одновременно с ростом популярности поделок от компании Apple, растет интерес к языку программирования ObjectiveC, который, наряду с библиотекой ObjectiveC-классов Cocoa, широко применяется для программирования под современными платформами Mac OS X (персональные компьютеры от Apple) и iOS (портативные устройства от Apple).

Случилось так, что недавно мне подвернулась удача познакомиться с этим вплотную и, даже, неплохо подзаработать именно на программировании в ObjectiveC (Cocoa), используя популярную в Mac OS X бесплатную среду разработки Xcode.

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

Итак, по порядку. Начнем с общих вопросов объектно-ориентированного программирования (ООП).

Современный стереотип ООП


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

Таково, на мой взгляд, типичное, и, я бы добавил, - стереотипное, представление об ООП сегодня. Более того, в большинстве книг, посвященных языкам Java, Си++ или современным вариантам Pascal, при обсуждении вопросов ООП, согласно моему опыту, не касаются других вопросов. Я немного сомневаюсь насчет языка C#, так как недостаточно хорошо его знаю, но, видимо, там все тоже самое. Отсюда и стереотип относительно ООП, утверждённый в наших головах изучением этих популярных сегодня языков программирования.

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

На самом деле, более полным повествованием, посвящённым вопросам ООП, будет то, в котором не только будет рассказано об объектах ООП к содержимому которых и относятся вопросы инкапсуляции, наследования и полиморфизма, но и будут затронуты вопросы взаимодействия этих объектов.

А взаимодействие объектов, как говорит наука, может быть в стиле function-oriented и в стиле message-oriented. Последний вариант взаимодействия объектов, по мнению этой науки, более кошерный, но он не нашел применения в языках Java, C++ и современных объектно-ориентированных диалектах Pascal. Однако, именно message-oriented стиль общения объектов используется в ObjectiveC и является вариантом общения в Qt.

Рассмотрим эти стили взаимодействия объектов более подробно.

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

class A {
public:
  void fun_1();
private:
  void fun_2();
};

A *a = new A();

a->fun_1(); // Напрямую обратились к public-методу fun_1() объекта a, 
            // созданного по образу и подобию класса A.
a->fun_2(); // Так делать нельзя!!! Метод fun_2() закрыт правилами инкапсуляции
            // для внешнего использования.

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

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

Альтернативным способом взаимодействия объектов является способ, ориентированный на сообщения - message-oriented. Этот случай можно представить в виде аналогии отправки почтой некоторого письма предназначенного некоторому адресату. Если почта не найдет адресата, то письмо вернется к вам со специальным уведомлением о том, что адресат не найден.

Способ совершенно безопасный, но требует поддержки в виде реализации некоторого почтового диспетчера в виде специального run-time объекта. Недостатком этого способа можно сразу назвать потери времени, требуемые на работу почтового диспетчера при каждом обращении к объектам.

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

Теперь, пришло время рассмотреть две известные реализации message-oriented взаимодействия, которые были построены для расширения языков C и C++. Речь, идет об ObjectiveC и Qt, соответственно.

Сообщения в ObjectiveC


ObjectiveC был создан в начале 80-х годов 20-го столетия Бредом Коксом (Brad Cox) в стиле минимальных расширений к языку C, целью которых была реализация поддержки ООП в варианте, навеянном языком Smalltalk.

Тут сразу можно заметить ещё одну существенную разницу между языками C++ и ObjectiveC. На этот раз, не с точки зрения взаимодействия объектов, а с точки зрения совместимости с самим C. Для ObjectiveC справедливо правило: "Любая программа на языке C является программой на языке ObjectiveC.". Для C++ последнее несправедливо.

Разумеется, если мы, по прошествии более 30 лет с момента появления ObjectiveC, говорим о нем как о языке, который является фактически основным языком программирования на популярных платформах от Apple, то понятно, что у него была относительно успешная история. Сегодня мне известны две реализации ObjectiveC: одна из них используется в Apple вместе с важнейшей для "яблочного" программотворчества библиотекой классов Cocoa, а вторая входит в состав коллекции компиляторов GNU и может быть установлена для работы в любом *nix. Вариант ObjectiveC от Apple можно тоже попробовать бесплатно, если использовать популярную для Mac OS X среду разработки Xcode.

Если вам интересно узнать историю языка и поверхностные подробности его синтаксиса и семантики, то, я бы посоветовал отличную статью в википедии по адресу http://ru.wikipedia.org/wiki/Objective-C. Здесь мы не будем повторяться и поговорим лишь о взаимодействии объектов.

В языке ObjectiveC, если у нас есть объект a, то мы можем послать ему сообщение message, например, таким образом:

[a message];

Для простоты даже не будем говорить о передаче параметров в сообщение, а поговорим только о том, как это работает. В составе средств ObjectiveC имеется специальная система run-time, в которой есть диспетчер, обрабатывающий все сообщения направляющиеся объектам. В рамках правил языка определяются жизненные циклы объектов, которые во всех подробностях известны этому диспетчеру. Таким образом диспетчер точно знает, существует ли в данный момент объект, которому назначается сообщение или нет. Кроме того, диспетчер знает о том, какие сообщения и с какими параметрами принимает каждый объект учитывая всю иерархию классов объекта. Таким образом, диспетчер знает, может ли данный объект обработать данное сообщение. В случае, если что-то не так, то сообщение переадресуется обратно отправителю. Если все нормально, то объект получает предназначенное ему сообщение. Теоретически, вы можете отправлять любую строку соответствующую синтаксису сообщения - диспетчер разберётся что с ней делать.

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

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

Сообщения в Qt


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

Неправильно это по двум серьезным причинам.

Во-первых, код Qt написан не на языке С++, а на специальном расширении этого языка, поддерживающим предварительную обработку кода специальным метакомпилятором (утилита moc - Meta Object Compiler). Правильным будет еще упомянуть утилиту uic (User Interface Compiler), которая преобразует некое XML-описание форм графического интерфейса, созданное графическим редактором форм, в код на Qt. Утилитой uic я не пользуюсь, предпочитая описывать интерфейс пользователя самостоятельно в коде программы.

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

История Qt, для меня, тесно пересекается с историей Java (0ak). Обе платформы были задуманы примерно в одинаковое время - в начале 90-х годов 20-го столетия. Наверное, годом рождения обоих идей и начальной реализацией можно назвать 1991 год. Кроме того, библиотеки классов, относящиеся к Qt и Java SE, на мой взгляд, многим похожи. Одна идея кроссплатформенна (Qt, попытка расширить ООП концепции языка C++), а вторая межплатформенна (Java, возможность реализовать ООП без наследия неудач C++).

Основателями Qt были программисты Хаавард Норд и Эрик Чамбенг. Первый, вскоре стал главным управляющим, а второй - президентом организованной ими компании Trolltech, которая и занималась развитием и продвижением Qt до января 2008 года, когда компания была выкуплена Nokia и была переименована в "Qt Software". Значение Qt в современном мире программирования трудно переоценить, о чем можно легко найти в Интернет много информации по запросам типа "история Qt".

Мы же перейдем к тому, что нас интересует сейчас и поговорим подробнее и слотах и сигналах.

Если какой-то объект, написанный в стиле Qt, хочет что-то безопасно сообщить другому объекту Qt, то он излучает сигнал, синтаксис которого описывается при создании объекта. Объект может излучать только те типы сигналов, которые он зафиксировал при своём описании. Сигнал определяется своей сигнатурой, которая определяется типами и последовательностью его параметров. Т.е. разные объекты могут эмитировать одинаковые сигналы - сигналы с одной и той же сигнатурой.

Приведем пример описания сигналов.

signals:
  void clicked();
  void itemChanged(int index);

Здесь описаны сигналы с разными сигнатурами. Сигнал clicked() не передает в себе параметров, а сигнал itemChanged(int index) передает целое значение.

Чтобы выполнить эмиссию сигнала в коде объекта, в котором сигнал описан, надо написать так:
emit clicked();
  ...
  int x = ...
  emit itemChanged(x);

Теперь, если некий объект, написанный в стиле Qt, хочет получить возможность реагировать на сигналы определённой сигнатуры, то он должен описать слот указанной сигнатуры.

Приведем пример того, как можно оформить слоты для описанных выше сигналов.

public slots:
  void first();
  void second(int index);

С точки зрения языка Си++, слоты являются обычными методами, т.е. где-то должна быть написана реализация этих методов. Более того, эти методы могут быть вызваны обычным образом, как и любой другой метод класса, учитывая правила инкапсуляции. Т.е. слоты, также как и другие члены класса, могут быть описаны с квалификаторами области видимости public, protected или private, только к ним добавляется ключевое слово slots, обрабатываемое метакомпилятором.

Теперь мы подошли к тому, как можно подключить сигнал одного объекта к слоту другого (или того же самого) объекта. Тут я провожу аналогию с электронными микросхемами, каждая из который имеет набор выходов и входов в согласии с некой спецификацией (аналогом сигнатуры). В случае электронных компонентов, мы проводниками разводим выводы одних объектов на входы других. В случае объектов Qt мы выполняем метаподключение сигнала одного объекта на слот другого объекта. Приведем пример как можно выполнить связываение двух сигналов объекта pObjectA с двумя сигналами объекта pObjectB, на основе примеров сигналов и слотов приведенных в пример ранее.

connect(pObjectA, SIGNAL(clicked()), pObjectB, SLOT(first()));
connect(pObjectA, SIGNAL(itemChanged(int)), pObjectB, SLOT(second(int)));

Связь сигнал-слот является связью многие ко многим. Наверное, это то, что существенно отличает его от электронного аналога, где подключение нескольких выходов на один вход является задачей нетривиальной.

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

Сравнение ObjectiveC и Qt


Я бы выделил три существенных различия в реализации message-oriented взаимодействия в ObjectiveC и Qt.

1. Для ObjectiveC передача сообщения является единственным способом обращения к методу объекта. Обращаться к полям объекта можно безо всяких фокусов, но такое обращение, в общем случае, некошерно, учитывая отсутствие надежных правил инкапсуляции данных в объектах ObjectiveC. Для объектов Qt общение через систему сигналов и слотов является не более чем расширением возможностей function-oriented возможностей взаимодействия объектов C++.

2. Сообщения ObjectiveC могут быть динамически созданы и переданы строкой, которая потом будет разобрана диспетчером сообщений, в то время как для Qt и сигналы и слоты должны быть описаны до этапа метакомпиляции.

3. Qt не имеет средств ускорения взаимодействия сигнал-слот за счет возможности запоминания хешированной сигнатуры сообщения.

Отдельно хочется заметить общие особенности между ObjectiveC и Ot. Оба эти артефакта расширяют существующие популярные языки программирования. ObjectiveC расширяет язык C, а Qt расширяет язык C++. В обоих случаях это расширение, фактически представлено предварительной метаобработкой кода и runtime поддержкой полученного кода при его исполнении специальными диспетчерами. Так же как "код на языке C является кодом для ObjectiveC", так же "код на языке C++ является кодом для Qt". Последнее свойство Qt я последнее время активно использую в разработке на языке C++. Мне настолько нравится среда разработки QtCreator, которая используется для кроссплатформенной разработки в Qt, что я также использую её и для обычных кроссплатформенных разработок на языке C++, используя в качестве кроссплатформенных средств сборки qmake и cmake.