четверг, 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)();
}