среда, апреля 04, 2007

Указатели на функцию, коллбэки и функторы

Иногда бывает нужно вызвать функцию по имени, представленном в виде текстовой строки. В URL'е это имя пришло, например. Или из текстового конфига вы его считали. Я долго искала какой-нибудь элегантный способ сделать это на С++, но ничего хорошего не нашла.

В JavaScript'е, например, для этого есть функция eval, которая пытается выполнить текстовую строку. Единственное решение, которое я нашла для C++: создавать некий массив, а лучше map с отображением символьного имени функции на указатель на нее.


void updateInfo()
{
...
}

map <string, void (*)()> mFunctions;
mFunctions["updateInfo"] = updateInfo;
//и так далее для всех функций, что нужно использовать
...
string sFunctionName;
...
//где-то там считали имя функции

mFunctions[sFunctionName]();//выполнили нужную функцию
mFunctions["updateInfo"]();//то же самое

И для каждой вновь появившейся функции придется руками этот массив править и перекомпилировать код.

Updated 11.04.2007
[Сразу несколько человек в комментариях заметили, что есть способ лучше, предложенный Александреску в "Современном проектировании".]

Ну и ладно. Зато появился повод поговорить об указателях на функции.
Указатели на функцию - вещь интересная и полезная. Настолько полезная, что появился отдельный сайт, посвященный исключительно указателям на функции - www.function-pointer.org. Сейчас он редиректит вот сюда: The Function Pointer Tutorials. Также в C++ FAQ Lite есть раздел, посвященный указателям на функции. Я пробегусь по основным моментам, но все равно почитайте эти ссылки там много интересно и полезного.

Указатель на функцию может выглядеть например так:

//объявляется указатель под именем pt2Function
int (*pt2Function)(float, char, char);

Указатель на функцию-член класса может выглядеть так.

//объявляется указатель под именем pt2Member
int (TMyClass::*pt2Member)(float, char, char);

Указатель на константную функцию-член класса может выглядеть так.

//объявляется указатель под именем pt2ConstMember
int (TMyClass::*pt2ConstMember)(float, char, char) const;
(Все примеры из The Function Pointer Tutorials)

И этот указатель на функцию можно всяко-разно использовать. Например, как я использовала в начале поста. Или его передать в функцию. И это получится уже коллбэк (callback).

void sort_ints(int* begin_items, int num_items,
int (*cmpfunc)(int, int) );
(пример из Wikipedia)

Это была объявлена функция сортировки, которая в для операции сравнения использует переданную cmpfunc.

Если вам вдруг нужно в качестве коллбэка передать нестатическую функцию-член класса, то, возможно, вы хотите странного. Но сделать это можно, вот объяснение как это сделать из C++ FAQ Lite, вот объяснение из The Function Pointer Tutorials.

Есть еще такое интересное понятие как функтор (functor, сокращение от function object).
В C++ можно сделать так: объявить класс, в котором переопределен operator(). Работа с таким объектом выглядит как работа с функцией. Но при этом это полноценный объект, возможностей у него больше, чем у функции. Он, например, может хранить состояние. В нем можно опеределить данные, другие функции и с ними работать.

template <class Fun>
void foo(Fun f)
{
//...
f();
//...
}

Обычно под функтором понимается любой объект, с которым можно работать как с функцией, то есть в C++ функтором являются и объекты, с переопределенным operator(), и собственно функции. Но тут я встречала разночтения. Иногда, когда говорят про функторы, обычные функции туда не включают (этот подход мне больше по душе). А иногда еще встречается термин функционоид (functionoid), для функторов, не являющихся обычными функциями.
Сочетание функторов с темплейтами приводит к очень интересным возможностям. Обратите внимание, в приведенном выше примере f может являться как функтором, так и указателем на функцию.

Ссылки:
comp.lang.c++.moderated Newbie question about operator() and functors in general
Функторы очень интенсивно используются в STL, о чем рассказывается здесь: Function Objects.
Очень хорошо, со всякими ссылками о функторах написано в Function object, Wikipedia
Указатели на функции-члены и реализация самых быстрых делегатов на С++
Указатели на функции члены класса

25 коммент.:

Nunquam dormio комментирует...

В С++, есть ещё тип такой - сигнатура. Помню с ним столкнулся, когда писал генератор парсеров для вызова функции по имени.
Причём задача была автоматический распознать количество параметров и их типы у функций. Вот тут пришлось извлекать сигнатуры и парсить их во время компиляции.

А вообще, конечно, проблема С++ в том, что функции в этом языке не являются объектами первого класса.
STL и основная часть boost`а пытаются компенсировать этот недостаток, добавить в С++ некоторые возможности функциональных языков.
Это, конечно, значительно увеличивает мощь языка. Но всё равно некоторые задачи решаются не так просто как бы хотелось.

Если бы С++ реализовывал внутренне хотя бы такие вещи как lambda-functions, currying, higher-order functions, то этому языку не было бы равных в прикладной области.

Vlad комментирует...

Помню, как мы с приятелем в годы студенчества долго и тщетно пытались объявить массив функций, тогда это у нас не очень получилось. И лишь сейчас, много лет спустя, я, наконец, узнал как это делается. Спасибо!

Victor комментирует...

Засыпался я как-то вопросом: Что будет, если указателю на функцию-член присвоить указатель на виртуальный метод предка, а потом вызвать её на экземпляре потомка?
http://singalen.livejournal.com/49410.html#cutid1

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

Mihhail комментирует...

Хороший обзор, спасибо.
Только линк на функторы от sgi поправьте :)

bolk комментирует...

В JS не нужно тут применять eval, там есть тип - функция (точнее - класс), даём значение переменной типа функция и вызываем.

Анонимный комментирует...

Александреску про это хорошо пишет - у него есть глава про "функторы"

Анонимный комментирует...

Ещё могу добавить интересные ссылки -
http://www.gamedev.ru/faq/?id=34
http://dobrokot.nm.ru/cpp/CppMethodsCallback.html

Ulysses комментирует...

functor, сокращение от function object

Не очень похоже на сокращение. Насколько я знаю, это термин из математики и к словосочетанию function object никакого отношения не имеет - просто Коплин, нимало не стесняясь, взял и позаимствовал его оттуда.

И еще мои 5 копеек в спор о том, входят ли в понятие функтор обыкновенные функции. На мои взгляд, однозначно нет: удобно иметь термин для обозначения классов с operator(). Когда я пишу программу на STL и сталкиваюсь с тем, что мне нужно передать алгоритму какое-либо действие, я говорю: «пора писать функтор» - в этом случае я не имею ввиду функцию, я имею ввиду класс.

Так же пишет и тот самый Коплин (которого, вслед за Эккелем – “TIC++”, vol.2, ch.10 Command, – считаю отцом данного понятия): «Функторы исполняют роль функций, но при этом создаются, передаются в параметрах и т. д. как объекты. Реализация функторов в С++ предельно проста: функтор оформляется в виде класса, содержащего всего одну функцию» (“Advanced C++ Programming Styles and Idioms”, 5.6).

icestudent комментирует...

> а лучше map с отображением символьного имени функции на указатель на нее.
А ещё лучше не указатель, а функтор (самая удобная и распространённая конструкция: boost::shared_ptr<boost::function<...> >).

Alena комментирует...

Хороший обзор, спасибо.

Всем пожалуйста, старалась :-)

Только линк на функторы от sgi поправьте :)

Поправила, спасибо.

Alena комментирует...

Не очень похоже на сокращение. Насколько я знаю, это термин из математики и к словосочетанию function object никакого отношения не имеет - просто Коплин, нимало не стесняясь, взял и позаимствовал его оттуда.

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

Анонимный комментирует...

в факе
http://www.parashift.com/c++-faq-lite/pointers-to-members.html

есть вопрос : "Могу ли я привести указатель на функцию к void*" . Был дан категорический ответ нет.
Бывают моменты когда это надо (например передавать указатель на callback функцию которая есть функцией класса (даже если ни есть членом класса)).В общем поптыку присваивание одного указателя другому (которое ни возможно осуществить из-за невозможности приведения типа) стоит заменить на побайтное копироние значения указателя посредствам хотя бы той же memcpy ();

Qbit комментирует...

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

...то можно сделать
так.

runtime комментирует...

Да Аноним правильно сказал, что у Александреску в "Современном проектировании" есть хороший пример реализации связи имени функции с указателем не нее через std::map. У него не надо это делать
"И для каждой вновь появившейся функции придется руками этот массив править и перекомпилировать код"

Анонимный комментирует...

Одно из самых интересных применений функторов - что-то типа этого кода:

doit( Find( "string" ) );

фишка в том, что Find() - это одновременно и конструктор функтора (который запоминает "string"), и адрес оператора (т.е. функции) этого функтора, который вызывается внутри doit() как алгоритм для выполнения поиска.

Прекрасную иллюстрацию кода можно найти в STL, поискав там реализацию алгоритмов типа foreach (не забудьте преобразовать код в читабельный вид:).

Пример:

template(*class _T*)
class Find
{
const _T &_searched_item;
public:
inline Find( const _T &rhs )
: _searched_item( rhs )
{}
inline bool operator()( const _T &rhs ) const
{ return( rhs == _searched_item ); }
};

// ps. замените (* и *) на треугольные скобки.

float_dublin комментирует...

про известное
int*(*(*get_it())())[]
{
return 0;
}

забыли :)

Adept комментирует...

Единственное решение, которое я нашла для C++: создавать некий массив, а лучше map с отображением символьного имени функции на указатель на нее.

И для каждой вновь появившейся функции придется руками этот массив править и перекомпилировать код.

[Сразу несколько человек в комментариях заметили, что есть способ лучше, предложенный Александреску в "Современном проектировании".]


Что-то я не понял насчёт лучше. Можно избежать надобности в перекомпиляции кода, если правильно код организовать, но это никак не отменит того, что нужно будет создавать ассоциативный массив и для каждой новой функции править его руками - выполнять добавление пары имя_функции-указатель_на_функцию (такие добавления необязательно сосредотачивать в одном месте).

Alena комментирует...

2Adept:

Что-то я не понял насчёт лучше.

Я не смогу пояснить... У меня есть планы добраться-таки до Александреску и разобраться с тем, что он предлагает по этому поводу, но пока я не разбиралась.

Black Angel комментирует...

Вот ещё интересная ссылка по указателям на функции. Советую почитать.
http://rsdn.ru/article/cpp/fastdelegate.xml

Анонимный комментирует...

Расскажите, кто конечно знает, подробнее про ссылки на функции. Например, ведь можно написать и так:

typedef T (& fREF)(T, T)

Александр комментирует...

Вот здесь http://www.dslev.narod.ru/PointersToMembers.htm синтаксис и применение интересное. Посмотрите кто желает.

Sergey комментирует...

После 15-минутного применения "метода математического тыка" выяснил как объявить массив указателей на функцию в куче

void (**fn_ptr)(int,float) = new (void(*[100])(int,float));

ScorpZ комментирует...

Я тут под g++ немного шаманил с указателями на ф-цию и преобразованием указателей.

-------------------
int *yyy = 0;
void (*fpoint) () = NULL;
fpoint = reinterpret_cast(yyy);
fpoint = (void (*)())yyy;
-------------------

Написал такое. и компилер мотюкается на 3 ю строку (4я - сишное преобразование нормально компилится). в принципе оно и понятно нефиг указатель на инт к указателю на ф-цию преобразовывать. Вопрос в другом, почему компилятор пропускает вот это -

fpoint = (void (*)())yyy;

Ведь все равно (я де то тут читал) сишное преобразование типов в конечном итоге сведется к то му же reinterpret_cast.

Анонимный комментирует...

А по моему и этот способ неплох. Это смотря где использовать.

Анонимный комментирует...

int*(*(*get_it())())[]
{
return 0;
}
void (**fn_ptr)(int,float) = new (void(*[100])(int,float));


Наркоманы штоле???