Шаг 30 - Функторы

Я снова в эфире. Это скорее всего последний Шаг в разделе "Идиомы и стили". Я больше ничего не знаю. Кроме того, у меня нет дома компа, а на работе (новой и противной) я слегка загружен. Примерно с 8 до 22. Хотите знать примерную статистику почты? Прислало отклики 30 человек, большинство из них - начинающие, а из них большинство спрашивали - стоит ли учить С++. Я всем вежливо и корректно отвечал, но правду сказал только одному. Но здесь не скажу. The truth is out there.

Как сохранить промежуточные данные между вызовами функции? Очень просто. Нужно использовать статические данные. Именно так делает, например, известная функция strtok(), разбивающая предложение на слова (упрощенно). К сожалению, в этом случае клиент функции должен свое "дело" обязательно доводить до конца, поскольку другой клиент, если получит управление, обязательно изменит статические данные. Если у Вас работает несколько потоков, можно применить критическую секцию или семафор, но это не решает проблему. Нужно очень четко следить, чтобы функция не была вызвана без вашего ведома, а это не всегда реально. Если Вы между двумя вызовами strtok() вызовете функцию из библиотеки, или создадите объект (и вызовете конструктор), или что-то еще, нет никакой гарантии, что там никто strtok() не вызывал.

С другой стороны, есть функции обратного вызова. Всякая функция, адрес которой вы получаете, и сохраняете где-то с надеждой вызвать ее в нужное время, по сути есть функция обратного вызова. Они применяются чрезвычайно широко в любых системах, где нужно программировать реакции на события. MFC не исключение: в карте сообщений вы указываете адреса функций, которые нужно вызвать при возникновении какого-то события. На www.codeguru.com валяется класс таймера, в котором для исполнения действия по интервалу нужно переписать функцию, или в лучшем (худшем) случае унаследовать его, чтоб по решению базового класса вызывалась переопределенная, виртуальная функция. Зачем? Можно же было просто передать адрес вызываемой функции, и дело с концом. Но вот несчастье, функция обратного вызова не помнит абсолютно ничего, и реинкарнирует каждый раз, когда ее вызывают. Верно, что можно положить внутрь ее статические данные, но Вы тогда вернетесь на абзац вверх: статический экземпляр бывает только один. А два таймера? Как они будут разбираться? Никак.

Решение в функторах. Используется возможность перегрузки оператора operator(). Определите класс CFunctor, частные данные которого будут использоваться как статические данные, и оператор operator(), который будет делать что-то нужное. Тогда для разных экземпляров класса CFunctor, например cf1 и cf2 эти данные будут, как обычно частными, но для каждого из вызовов cf1() и cf2() (а это одна и та же функция, только с разными именами) эти данные будут статическими - то есть сохраняться от момента создания экземпляра до уничтожения.

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

// funct.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
// Это подключение пространства имен std
using namespace std;
// сам функтор
class CFunctor
{
public:
	CFunctor (int _pStat = 0):m_pStat (_pStat){};
	const int& operator() ( int _i=0) { return m_pStat+=_i;};
	const int& readval () {return m_pStat;};
private:
	int m_pStat;
};

// таймер
class CTimer
{
public:
	// установка функции обратного вызова - функтора
	void setcallback (CFunctor);
private:
	CFunctor m_cf;
	void onTick(void);
};
// реализации
void CTimer::setcallback (CFunctor _cf)
{
	m_cf = _cf;
}
void CTimer::onTick(void)
{
	m_cf();
}


// здесь попробуем как действует
int main(int argc, char* argv[])
{
	CFunctor f1(6);
	CFunctor f2(100);

	CTimer ct1, ct2;

// Это - анонимные экземпляры функторов. Они живут одно 
// мгновение, и сейчас исчезнут. Но их копии останутся внутри таймеров
// до поры до времени.
	ct1.setcallback (CFunctor(5));
	ct2.setcallback (CFunctor(66));

	cout << f1(2)<< endl;
	cout << f1(3)<< endl;

	cout << f2(200)<< endl;
	cout << f2(300)<< endl;

	// Здесь якобы ваш цикл обработки сообщений
	// for (;;) {}

	return 0;
}

Что здесь происходит: f1, f2 - два функтора одного типа, создаются и ждут своего часа. ct1, ct2 - два таймера, создаются, и оба конфигурируются экземплярами класса CFunctor, только со своими параметрами - и эти параметры сохраняются для каждого таймера по отдельности! Учтите, кстати, что функторы передаются по значению, ибо неименованные экземпляры уничтожатся немедленно!

Есть еще применение функторам: их можно вместо функций-предикатов передавать в шаблоны алгоритмов в STL. Тогда при каждом вызове можно чего-нибудь там делать... но что, хоть убей не знаю. Посмотрите у Леена Амерааля, если интересно.

Краткое резюме: функторы есть хорошая замена глобальным функциям со статическими данными, поскольку позволяют иметь несколько независимых экземпляров этих статических данных.

А теперь прописные истины:

Программист! Если ты заодно и сисадмин, брось все дела! Вскочи и немедленно побеги к серверу, сделай загрузочные дискеты! Сделай resque disk с критической информацией по серверу! Сделай resque disk для RAID массива! Проверь работу резервного копирования, да не так, как всегда, а попробуй восстановить с копии! Напиши реальный план, что ты будешь делать, если в сервак ударит молния! Отремонтируй UPS! Немедленно!

Кстати, я не шучу. Мне не смешно. На четвертый день работы на новом месте это произошло у нас, и мы не подняли текущую базу, а только суточную, и я не думаю, что за это мне (лично мне) выпишут дополнительную премию.

Засим позвольте мне откланяться.

Искренне Ваш 
Альберт Махмутов.
ICQ_7863642

Предыдущий Шаг | Оглавление
Автор Albert Makhmutov.