В этой статье на примере реализации механизма обратного вызова будет рассмотрена возможности применения лямбда функций в удобной и быстрой форме.
Постановка задачи
Необходимо реализовать удобный и быстрый механизм сохранения «указателя» на произвольную функцию и последующего его вызова с передачей аргумента (для примера возьмём тип 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 %dn",f1(5),f1(-10));
printf("%f %fn",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»).
У такой реализации единственное ограничение – это максимальный размер захватываемых в лямбда функциях переменных. Но обычно требуется передать не так много дополнительных параметров. А при большом количестве можно и использовать динамическую память в ущерб скорости.
Итог
Удобство и функционал передачи функции как указателя был доведён до высокого уровня удобства без особого увеличения ресурсоёмкости. Что касается функционала, то простора для творчества ещё предостаточно: создание очереди с приоритетами (потока событий), шаблона для разных типов аргумента и т.д.
Автор: DreamNik