Постановка задачи
Необходимо реализовать удобный и быстрый механизм сохранения «указателя» на произвольную функцию и последующего его вызова с передачей аргумента (для примера возьмём тип 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/
Добавить комментарий