Лямбда-функции и реализация удобного механизма Callback-ов на C++

от автора

В этой статье на примере реализации механизма обратного вызова будет рассмотрена возможности применения лямбда функций в удобной и быстрой форме.

Постановка задачи

Необходимо реализовать удобный и быстрый механизм сохранения «указателя» на произвольную функцию и последующего его вызова с передачей аргумента (для примера возьмём тип char*).

Метод 1 – на классическом «Си»

Решая задачу «в лоб» можно получить что-то вроде такого:

//Определение нашей функции static void MyFunction(char *s){ 	puts(s); }  int main(){ 	//Переменная, хранящая указатель на функцию 	void (*MyCallback)(char *argument);  	//Сохранение указателя на нашу функцию 	MyCallback=MyFunction;  	//Вызов функции по указателю 	MyCallback("123");  	return 0; } 

Механизм очень простой и часто используемый. Но при большом количестве обратных вызовов их объявление становиться не очень удобным.

Лямбда-функции в С++

Для тех кто не слышал про С++11 (или С++0x) или пока ещё не коснулся его, расскажу про некоторые нововведения из этого стандарта. В С++11 появилось ключевое слово auto, которое может ставиться вместо типа при объявлении переменной с инициализацией. При этом тип переменной будет идентичен типу, казанному после «=». Например:

	auto a=1;     // тоже самое что  int         a=1; 	auto b="";    // тоже самое что  const char* b=1; 	auto c=1.2;   // тоже самое что  double      c=1; 	auto d;	// ошибка! невозможно определить тип переменной d 

Но самое интересное это лямбда-функции. В принципе, это обычные функции, но которые могут быть объявлены прямо в выражении:

[](int a,int b) -> bool //лямбда функция с двумя аргументами, возвращает bool { 	return a>b; } 

Синтаксис лямбда функции таков:

[захватываемые переменные](аргументы)->возвращаемый тип{ тело функции } 

Кусок «->возвращаемый тип» может отсутствовать. Тогда подразумевается «->void». Ещё пример использования:

int main(int argc,char *argv[]){  	//функция, аналогичная abs(int) 	auto f1=[](int a)->int{ 		return (a>0)?(a):(-a); 	};  	//функция, возвращающая случайное значение от 0.0 до 1.0 	auto f2=[]()->float{ 		return float(rand())/RAND_MAX; 	};  	//функция, ожидающая нажатия enter 	auto f3=[](){ 		puts("Press enter to continue..."); 		getchar(); 	};  	printf("%d %d\n",f1(5),f1(-10)); 	printf("%f %f\n",f2(),f2()); 	f3();  	return 0; } 

Данная программа выведет:

5 10 0.563585 0.001251 Press enter to continue... 

В этом примере были объявлены и проинициализированы три переменные (f1,f2 и f3) типа auto, следовательно тип которых соответствует типу стоящему справа – типу лямбда функций.
Лямбда функция, сама по себе, не является указателем на функцию (хотя в ряде случаев может бытьприведена к нему). Компилятор вызывает функцию не по адресу а по её типу – именно поэтому у каждой лямбда функции свой тип, например «<lambda_a48784a181f11f18d942adab3de2ffca>». Такой тип невозможно указать, поэтому его можно использовать только в связке с auto или шаблонами (там тип тоже может автоматически быть определён).
Стандарт так же допускает преобразование от типа лямбда к типу указателя на функцию, в случае отсутствия захватываемых переменных:

void(*func)(int arg); func= [](int arg){ ... }; // была лямбда, стала указатель 

Захватываемые переменные это те переменные, которые «попадают внутрь» лямбда функции при её указании:

int main(int argc,char *argv[]){ 	auto f=[argc,&argv](char *s){ 		puts(s); 		for(int c=0;c<argc;c++){ 			puts(argv[c]); 		} 	};  	f("123");  	return 0; } 

Эти параметры, фактически и сохраняются (копируются по значению) в переменной f.
Если указать знак & перед именем, то параметр будет передан по ссылке, а не по значению.
Адрес самой функции по-прежнему нигде не хранится.

Метод 2 – Реализация на С++

Заменив статическую функцию на лямбду можно упростить наш пример:

int main(){ 	void (*MyCallback)(char *argument);  	//Теперь функция может быть определена прямо здесь! 	MyCallback=[](char *s){ 		puts(s); 	};  	MyCallback("123");  	return 0; } 

Вот так немножко добавив «плюсов» можно сильно упростить жизнь, главное не переборщить, чем мы сейчас и попробуем заняться. В этом примере такая конструкция будет работать, пока нам не захочется «захватить» переменные в лямбда функции. Тогда компилятор не сможет преобразовать лямбду в указатель. Вот тут, используя С++ можно сделать так:

class Callback{ private:  	// Класс, обеспечивающий вызов функций с их особенностями 	class FuncClass{ 	public: 		// Переопределяемая функция 		virtual void Call(char*)=0; 	};  	// Указатель на сохранённый класс 	FuncClass *function;  public:  	Callback(){ 		function=0; 	}  	~Callback(){ 		if(function) delete function; 	}    	template<class T> 	void operator=(T func){ 		if(function) delete function;  		// Класс с переопределённой функцией Call, вызывающей func 		class NewFuncClass:public FuncClass{ 		public: 			T func;  			NewFuncClass(T f):func(f){ 			}  			void Call(char* d){ 				func(d); 			} 		};  		// Создаём экземпляр класса и сохраняем его 		function=new NewFuncClass(func); 	}  	void operator()(char* d){ 		if(function) function->Call(d); 	} };  int main(){  	Callback MyCallback;  	MyCallback=[](char *s){ 		puts(s); 	};  	MyCallback("123");  	return 0; } 

Вот так. Чуть-чуть плюсов и код в несколько раз больше. Громоздкая реализация, а ведь здесь ещё и не учтена возможность копирования экземпляров Callback. Но удобство использования на высоте. Так же за скромной операцией «=» прячется выделение динамической памяти, да ещё конструктор – явно не вписывается в концепцию наглядности кода широко любимую верных классическому «Си» программистам.

Попробуем это исправить и максимально ускорить и упростить реализацию, не потеряв удобство.

Метод 3 – Что-то среднее

Реализация:

class Callback{ private: 	void (*function)(char*,void*); 	void  *parameters[4];  public: 	Callback(){ 		function=[](char*,void*){ 		}; 	}  	template<class T> 	void operator=(T func){ 		// Вот так мы убедимся, что  sizeof(T) <= sizeof(parameters) 		// Если это не выполняется, то будет compile-time ошибка, т.к. 		// нельзя указывать отрицательный размер массива 		sizeof(int[ sizeof(parameters)-sizeof(T) ]);  		// Сохраняем указатель на функцию, которая вызывает переданную функцию func 		function=[](char* arg,void *param){ 			(*(T*)param)(arg); 		};  		// Копируем значение в переменной func в parameters 		memcpy(parameters,&func,sizeof(T)); 	}  	void operator()(char* d){ 		// Вызываем функцию по указателю function, передав ещё и parameters 		function(d,parameters); 	} };  int main(){  	Callback MyCallback;  	MyCallback=[](char *s){ 		puts(s); 	};  	MyCallback("123");  	return 0; } 

Во первых: мы убрали большой кусок связанный с виртуальными функциями и выделением памяти. Сохранение просиходит на скорости копирования нескольких байт.

Вызов тоже быстрый – от вызова двух вложенных функций (вспомогательная и сохранённая) до одной, когда компилятор встраивает одну в другую – почти идеальный вариант (от идеала отделяет один лишний аргумент «parameters»).

У такой реализации единственное ограничение – это максимальный размер захватываемых в лямбда функциях переменных. Но обычно требуется передать не так много дополнительных параметров. А при большом количестве можно и использовать динамическую память в ущерб скорости.

Итог

Удобство и функционал передачи функции как указателя был доведён до высокого уровня удобства без особого увеличения ресурсоёмкости. Что касается функционала, то простора для творчества ещё предостаточно: создание очереди с приоритетами (потока событий), шаблона для разных типов аргумента и т.д.

ссылка на оригинал статьи http://habrahabr.ru/post/159003/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *