Манипуляции с методами классов, или как вызвать функцию по ее символьному имени.
Вячеслав Ермолаев. http://bcdev.narod.ru
e-mail:yerm@mail.ru

В качестве отрицательных сторон библиотеки VCL  часто называется ее дельфийское происхождение, что приводит к тому , что по некоторым моментам  поведение объектов VCL-классов не соответствует стандарту С++. К таким моментам в частности можно отнести своеобразный порядок вызова конструкторов базовых « дельфийных» классов, поведение виртуальной функции при вызове ее в теле конструктора,  ограничения, накладываемые при использовании  множественного наследования ( до появления  С++Builder 6  разговор велся не просто об ограничении, а о недопустимости применения множественного наследования для VCL-классов).

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

Не стоит думать, что  применения  указателя  на метод объекта  узко ограничено лишь областью VCL классов.  Подобные  указатели , хотя и не часто, встречаются в программисткой  практике и оказываются довольно полезны. Имеются реализации таких указателей с использованием стандартного С++.[Александреску А. Современное проектирование на С++, М.Издательский дом «Вильямс», 2002.] В С++Builder программист получает эти возможности даром, в качестве своеобразной компенсации  за «моральный ущерб» в результате потери совместимости со стандартом.

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

Вот программа-минимум, которую мы должны выполнить:

-          вызов обычной функции как метода в  качестве обработчика события;

-          вызов метода как обычной функции;

-          вызов опубликованного ( published)  метода по его символьному имени;

-          получение имени опубликованного метода.

В соответствии с этими задачами наша форма примет следующий вид:

Рис.1

 

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

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

Для определения метода просто зададим обработчик OnClick для верхней кнопки Button1 и в сгенерированном шаблоне наберем  код   тела  метода:

//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)

{

   ShowMessage(AnsiString("Метод:")+

    this->Name + "->" + ((TComponent*)Sender)->Name);

}   

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

Теперь определим обычную функцию GlobalClick:

void __fastcall GlobalClick(void* This, TObject *Sender)

{

    ShowMessage(AnsiString("Функция:")+

    ((TComponent*)This)->Name + "->" +

    ((TComponent*)Sender)->Name);

}  

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

Отметим следующую особенность : у функции в отличии от метода добавился  еще один параметр типа void*. Через этот параметр будет передаваться указатель на объект класса. В нашем случае, через него будет передаваться указатель на Form1.

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

Замечание. События (events) реализуются в С++Builder посредством указателей на метод объекта (__closure), то есть события являются по сути указателями на метод объекта

 Эту операцию выполним в теле конструктора формы:

_fastcall TForm1::TForm1(TComponent* Owner)

    : TForm(Owner)

{

   TMethod Method;

   Method.Data = this;

   Method.Code= GlobalClick;

   Button2->OnClick = *(TNotifyEvent*)&Method;

}

При выполнении этой операции задействована структура TMethod. Структура довольна проста и содержит всего лишь два поля Data и Code,  которые имеют тип void*

struct TMethod

{

      void *Code;

      void *Data;

};

и  по размеру соответствует указателю на метод объекта, состоящего , как уже упоминалось выше, также из двух указателей . Следовательно посредством этой структуры с помощью преобразования можно инициализировать указатель на метод нужным значением. Для этого предварительно в поле Data заносится адрес объекта (в данном случае это форма), в поле Code  - адрес функции. После присвоения содержимого Method событию OnClick данное событие будет связано  с функцией, и при нажатии на кнопку Button2 в процессе работы программы  механизм выполнения событий вызовет функцию GlobalClick и передаст ей в качестве параметров адрес на Form1 и адрес на объект, запустивший событие. В нашем случае этим объектом будет кнопка Button2. Результат работы программы показан на рис.2

Рис.2

Вызов метода как обычной функции выполним  в теле обработчика  кнопки Button3:

void __fastcall TForm1::Button3Click(TObject *Sender)

{

    //вызвать метод как обычную функцию

    TNotifyEvent Click = &Button1Click;

    TMethod Method = *(TMethod*)&Click;

    //через первый скрытый параметр передаем this

    typedef void (__fastcall *Func)(void*,TObject *);

    Func func;

    func = (Func)Method.Code;

    func(this, Sender);

}    

     Данная операция выполняется в обратном порядке. Переменной Method присваивается содержимое указатель на метод. Затем поле Code приводим к типу: указатель на функцию параметрами  типа void*, TObject* и выполняем вызов функции, передав ей в качестве  параметров this и Sender. Результат работы  программы можно увидеть на рис.3

 Рис.3

Для выполнения двух остальных задач потребуется задействовать дополнительные возможности расширенного RTTI, которые достались по наследству от Delphi  и реализованы в виде методов класс TObject. Естественно, эта дополнительная информация доступна только для VCL-классов, то есть только тех классов, которые являются производными от TObject. Нам потребуются  пока только два метода. Это     

void * __fastcall MethodAddress(const ShortString &Name);

и

ShortString __fastcall MethodName(void *Address);

С помощью первого из них можно получить адрес  метода по его символьному имени, с помощью второго – получить имя, зная адрес. При этом надо учитывать, что использовать эти функции можно только для опубликованных методов, то есть методов, которые размещены  в секции __published.

Вызов метода по имени выполним  в теле обработчика  кнопки Button4, а получение имени метода в  теле обработчика Button5:

void __fastcall TForm1::Button4Click(TObject *Sender)

{

    ShortString ProcName = "Button1Click";

    TMethod Method = { MethodAddress(ProcName), this } ;

    if (Method.Code)

    {

        TNotifyEvent Click = *(TNotifyEvent*)&Method;

        Click(Sender);

    }

}

//---------------------------------------------------------------------------

void __fastcall TForm1::Button5Click(TObject *Sender)

{

    TMethod Method = *(TMethod*)&(Button1->OnClick);

    ShowMessage( MethodName(Method.Code));

}

 

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



Сайт управляется системой uCoz