Делегаты, сигналы и слоты.
Как известно, в C++ делегатов нет. А жаль.
Впринципе, в интернете много делегатов. И слотов много. Но, я зачем-то решил написать свои.
Благодаря механикам: официальной — variadic templates и неофициальной — horrible_cast реализация делегатов оказалось компактной, хотя, у нее, возможно и будут проблемы с переносимостью (не зря же у нормальных людей в реализациях трёхэтажная шаблонная магия и двухэтажная препроцессорная).
Платформа на которой всё запустилось — avr2560 в виде Ардуино. К статье прилагаю сию работу в виде Ардуино библиотеки.
Значительно упрощает реализацию делегата отказ от использования множества различных сигнатур применительно к одному делегату. (Хоть убейте, не знаю, зачем оно людям нужно). Действительно, если объявить делегат шаблонным с самого начала и явно прописать его сигнатуру, не приходится изобретать хитрых методов востановления сигнатуры.
Механика сохранения информации делегата у нас следующая следующая.
1. Делегат инициализируется парой указатель на объект и указатель на метод класса.
2. Указатель на объект приводится к указателю на тип Abstract.
3. Указатель на метод класса приводится к указателю на метод класса Abstract (спасибо horrible_cast)
4. Полученные указатели сохраняются в отведённых полях.
В этом процессе мы теряем информацю о типе объекта, но оказывается, что для корректного вызова метода тип знать совсем не обязательно.
Механика вызова делегата и того проще. Мы просто вызываем с передаваемыми оператору вызова параметрами метод, якобы, Abstract класса по сохранённому указателю на метод и сохранённому указателю на объект. Удивительно, но это работает.
Собственно, вот что из себя представляет основа делегата:
Вот типичный конструктор:
Вот вызов:
Всё… Если вы откроете файл delegate.h в выложенной мной библиотеке, вы увидите, что реализация немного сложнее из-за некоторых дополнительных механизмов инициализации и из-за добавления возможности вызова обычных, не являющихся членами класса функций. У них точно такая же сигнатура, так что проблем с этим не возникает.
Использование:
Первый параметр шаблона — возвращаемое значение. Дальше типы параметров в любом количестве.
Макрос method введён специально для передачи значений в конструктор через знак присваивания.
(операторы присваивания неопределены, а потому присваивание вызывает конструктор).
Пожалуй, здесь всё.
На основе данной реализации делегатов и механики monolist строится sigslot библиотека. Класс sigslot — класс сигнала. В качестве слота выступает любая функция или метод с такой же сигнатурой.
Вызов sigslot через operator() приводит к последовательному выполнению всех находящихся в списке делегатов (с переданными параметрами, разумеется).
Библиотека использует динамическую память для хранения делегатов. Алгоритм работы, вероятно, медленней других реализаций, особенно разворачивающихся на этапе компиляции. В данной реализации вся работа выполняется в реалтайме.
Библиотека перегружает для класса sigslot операторы
+= — добавление метода или функции.
!= — приоритетное добавление метода.
-= — удаление метода.
По умолчанию последний добавленный будет последним же исполняться. Приоритетное добавление ставит добавляемую функцию в начала списка. Такая функция исполняется первой.
Удаление метода заключается к поиску в списке записи, аналогичной переданной функции и удалению ее.
При вызове сигнал возвращает то, что вернул последний объект. Не знаю, зачем…
Пример использования:
Рассказывать о том, как оно работает я не буду, ибо реализация чуть менее чем полностью состоит из нудных операций по работе со списками и ничего реально интересного, кроме того, что я уже рассказал там нет.
Если вы до сюда дочитали, спасибо.
Прилагаю Ардуино библиотеку с реализацией. В файлах приводится довольно подробный комментарий о том, что зачем и с какими параметрами вызывается.
Впринципе, в интернете много делегатов. И слотов много. Но, я зачем-то решил написать свои.
Благодаря механикам: официальной — variadic templates и неофициальной — horrible_cast реализация делегатов оказалось компактной, хотя, у нее, возможно и будут проблемы с переносимостью (не зря же у нормальных людей в реализациях трёхэтажная шаблонная магия и двухэтажная препроцессорная).
Платформа на которой всё запустилось — avr2560 в виде Ардуино. К статье прилагаю сию работу в виде Ардуино библиотеки.
Значительно упрощает реализацию делегата отказ от использования множества различных сигнатур применительно к одному делегату. (Хоть убейте, не знаю, зачем оно людям нужно). Действительно, если объявить делегат шаблонным с самого начала и явно прописать его сигнатуру, не приходится изобретать хитрых методов востановления сигнатуры.
Механика сохранения информации делегата у нас следующая следующая.
1. Делегат инициализируется парой указатель на объект и указатель на метод класса.
2. Указатель на объект приводится к указателю на тип Abstract.
3. Указатель на метод класса приводится к указателю на метод класса Abstract (спасибо horrible_cast)
4. Полученные указатели сохраняются в отведённых полях.
В этом процессе мы теряем информацю о типе объекта, но оказывается, что для корректного вызова метода тип знать совсем не обязательно.
Механика вызова делегата и того проще. Мы просто вызываем с передаваемыми оператору вызова параметрами метод, якобы, Abstract класса по сохранённому указателю на метод и сохранённому указателю на объект. Удивительно, но это работает.
Собственно, вот что из себя представляет основа делегата:
template<typename OutputType ,typename ... VarTypes>
class delegate
{public:
class Abstract{};
typedef Abstract* delegate_obj_t;
typedef OutputType (Abstract::*delegate_mtd_t)(VarTypes ...);
delegate_obj_t object;
delegate_mtd_t method;
....
Вот типичный конструктор:
//Конструктор. Делегат метода класса.
//@1 указатель на объект, метод которого вызывается.
//@2 указатель на метод.
//Пример delegate<void, int> d(&a, &A::func);
template<typename T1, typename T2>
delegate(T1* ptr_obj, OutputType(T2::*mtd)(VarTypes ...)) : function(0)
{
object = reinterpret_cast <delegate_obj_t> (ptr_obj);
method = horrible_cast<delegate_mtd_t, OutputType(T2::*)(VarTypes ...)>(mtd);
};
Вот вызов:
OutputType operator()(VarTypes ... arg) {
return (object->*method)(arg ...);
};
Всё… Если вы откроете файл delegate.h в выложенной мной библиотеке, вы увидите, что реализация немного сложнее из-за некоторых дополнительных механизмов инициализации и из-за добавления возможности вызова обычных, не являющихся членами класса функций. У них точно такая же сигнатура, так что проблем с этим не возникает.
Использование:
class A
{
void mtd(int){};
} a;
void fnc(int){};
void fnc2(int){};
delegate<void, int> d1 = fnc;
d1(3);
d1 = fnc2;
d1(4);
delegate<void, int> d2(&a, &A::mtd);
delegate<void, int> d3 = method(a, A::mtd);
Первый параметр шаблона — возвращаемое значение. Дальше типы параметров в любом количестве.
Макрос method введён специально для передачи значений в конструктор через знак присваивания.
(операторы присваивания неопределены, а потому присваивание вызывает конструктор).
Пожалуй, здесь всё.
На основе данной реализации делегатов и механики monolist строится sigslot библиотека. Класс sigslot — класс сигнала. В качестве слота выступает любая функция или метод с такой же сигнатурой.
Вызов sigslot через operator() приводит к последовательному выполнению всех находящихся в списке делегатов (с переданными параметрами, разумеется).
Библиотека использует динамическую память для хранения делегатов. Алгоритм работы, вероятно, медленней других реализаций, особенно разворачивающихся на этапе компиляции. В данной реализации вся работа выполняется в реалтайме.
Библиотека перегружает для класса sigslot операторы
+= — добавление метода или функции.
!= — приоритетное добавление метода.
-= — удаление метода.
По умолчанию последний добавленный будет последним же исполняться. Приоритетное добавление ставит добавляемую функцию в начала списка. Такая функция исполняется первой.
Удаление метода заключается к поиску в списке записи, аналогичной переданной функции и удалению ее.
При вызове сигнал возвращает то, что вернул последний объект. Не знаю, зачем…
Пример использования:
s += func;
s += method(a, A::mtd);
s(3, 0.97);
Рассказывать о том, как оно работает я не буду, ибо реализация чуть менее чем полностью состоит из нудных операций по работе со списками и ничего реально интересного, кроме того, что я уже рассказал там нет.
Если вы до сюда дочитали, спасибо.
Прилагаю Ардуино библиотеку с реализацией. В файлах приводится довольно подробный комментарий о том, что зачем и с какими параметрами вызывается.
- +3
- 09 декабря 2015, 11:48
- Mirmik
- 1
Файлы в топике:
sigslot.zip
Я тоже использую подобные делегаты. Несколько рекомендаций:
1. Делегат инициализированный по умолчанию нельзя вызавать — будет доступ по нулевому указателю. Лучше по умолчанию инициализировать делегат пустой функцией, тогда и необходимость проверки отпадет.
2. Нет способа проверить, что делегат пустой и его нельзя вызывать.
3. Вызов функции члена класса по указателю не очень эффективен, порядка 15-30 тактов если без параметров.
4. Чтобы вызвать такой делегат из старого Си-шного кода (а иногда такое надо), придется вручную делать фукнцию обертку.
Есть способ лучше:
1. Делегат инициализированный по умолчанию нельзя вызавать — будет доступ по нулевому указателю. Лучше по умолчанию инициализировать делегат пустой функцией, тогда и необходимость проверки отпадет.
2. Нет способа проверить, что делегат пустой и его нельзя вызывать.
3. Вызов функции члена класса по указателю не очень эффективен, порядка 15-30 тактов если без параметров.
4. Чтобы вызвать такой делегат из старого Си-шного кода (а иногда такое надо), придется вручную делать фукнцию обертку.
Есть способ лучше:
#include <iostream>
template<class Result>
class Delegate
{
public:
typedef Result (*InvokeT)(void *); // указатель на функцию (Invoke())
static Result VoidFunc() { return Result(); }
private:
void *_object;
InvokeT _callback;
template<class ObjectT, Result (ObjectT::* Func)()>
static Result InvokeObj(void *object){
return (static_cast<ObjectT*>(object)->* Func)();
}
template<Result (* Func)()>
static Result InvokeFunc(void *){
return Func();
}
Delegate(void *obj, InvokeT func)
: _object(obj), _callback(func) { }
public:
Delegate()
: _object(0), _callback(&InvokeFunc<VoidFunc>) { }
// вызывает конструктор,
// для каждой функции класса создаёт свой Invoke (т.к. специализация шаблона!)
// и передаёт указатель на Invoke как второй параметр ctor"а.
template<class ObjectT, Result (ObjectT::* Func)()> static Delegate Make(ObjectT * object){
return Delegate(object, &InvokeObj<ObjectT, Func>);
}
template<Result (* Func)()> static Delegate MakeFree(){
return Delegate(0, &InvokeFunc<Func>);
}
// вызов Invoke и передача ему объекта для которого он был создан
inline Result operator()()const{
return _callback(_object);
}
InvokeT ToCFunction()const{
return _callback;
}
void* Object()const{
return _object;
}
};
class Foo
{
public:
bool Bar(){ std::cout << "Hello" << std::endl; return true;}
};
int main()
{
Foo foo;
Delegate<bool> d;
d();
d = Delegate<bool>::Make<Foo, &Foo::Bar>(&foo);
d();
return 0;
}
e_mc2
3. Вызов функции члена класса по указателю не очень эффективен, порядка 15-30 тактов если без параметров.
Так вы же тоже вызываете метод по указателю на метод 0_о… Или я что-то недопонял понял.<br /
1. Делегат инициализированный по умолчанию нельзя вызавать — будет доступ по нулевому указателю. Лучше по умолчанию инициализировать делегат пустой функцией, тогда и необходимость проверки отпадет.
Да и фиг с ним, честно говорю… Но, это да, пожалуй надоработка.
3. Вызов функции члена класса по указателю не очень эффективен, порядка 15-30 тактов если без параметров.
Так вы же тоже вызываете метод по указателю на метод 0_о… Или я что-то недопонял понял.<br /
1. Делегат инициализированный по умолчанию нельзя вызавать — будет доступ по нулевому указателю. Лучше по умолчанию инициализировать делегат пустой функцией, тогда и необходимость проверки отпадет.
Да и фиг с ним, честно говорю… Но, это да, пожалуй надоработка.
Одно дело вызывать функцию по обычному указателю на функцию, и совсем другое вызывать функцию член класса(методов в С++ таки нет) по указателю на нее.
У меня в функцию-обёртку InvokeObj указатель на функцию член класса передается как параметр шаблона и известен на этапе компановки. В худшем случае такой вызов будет обычным вызовом по известному адресу, в лучшем — вызов вообще заинлайнится.
У меня в функцию-обёртку InvokeObj указатель на функцию член класса передается как параметр шаблона и известен на этапе компановки. В худшем случае такой вызов будет обычным вызовом по известному адресу, в лучшем — вызов вообще заинлайнится.
В теории ООП обычно говорят о «данных» и «методах обработки данных». В стандарте С++ нет понятия «метод», там есть Member functions (и т. д.), хотя в разговорной речи намного чаще употребляют «метод». Точно также, как часто путают класс и объект (экземпляр класса), когда говорят «создаем класс» хотя создают его экземпляр. В общем (ИМХО), это тонкости из серии «нажал на курок» (хотя на самом деле на спусковой крючок), «просверлил дырку» (отверстие) и т. д.
Есть мнение, что true метод должен быть «объектом первого класса». Хотя в терминологии как всегда разброд и шатание и где-то исторически сложилось, что функции зовут методами, а где-то наоборот.
Присоединяюсь к вопросу коллеги Vga
Я нигде не встречал подобного мнения. Хотя действительно теория ООП дает весьма расплывчатые термины и их часто трактуют по разному.
Я нигде не встречал подобного мнения. Хотя действительно теория ООП дает весьма расплывчатые термины и их часто трактуют по разному.
Одно дело вызывать функцию по обычному указателю на функцию, и совсем другое вызывать функцию член класса(методов в С++ таки нет) по указателю на нее.А в чем разница, кстати? Вроде ж ФЧК отличается только передачей дополнительного параметра — this.
Разница в том, что когда вызов идет по указателю на ФЧК, в общем случае не известно что за функция вызывается. Это может быть обычная ФЧК, может виртуальная виртуальная, плюс надо учитывать наследование, множественное и виртуальное. Указатель на ФЧК — это хитрая структура с несколькими полями и флагами, размером с два обычных указамеля для GCC и даже больше для некоторых других компиляторов.
Есть неплохая статьяпро указатели на ФЧК и делегаты, где-то были её переводы на русский, если надо.
Есть неплохая статьяпро указатели на ФЧК и делегаты, где-то были её переводы на русский, если надо.
^^
Правда с виртуальными функциями мы больше не работаем.
OutputType operator()(VarTypes ... arg) {
uint8_t type = (object == (delegate_obj_t) -1) ? FUNCTION : METHOD;
switch (type)
{
case METHOD:
//return (object->*method)(arg ...);
return (reinterpret_cast<OutputType(*)(void*,VarTypes ... arg)> (method))(object, arg ...);
case FUNCTION:
return (function)(arg ...);
};
};
Правда с виртуальными функциями мы больше не работаем.
Это очень не надежно. На x86 такое уже не будет работать. Да и на АВР может сломаться в зависимости от настроек компилятора.
amaora
Построение универсального интерфейса без применения наследования.
По сути мы оборачиваем объект и метод любого типа в обертку делегата. Дальше мы можем работать просто с делегатом, не особо задумываясь о многообразии типов.
Пример — класс таймеров. Функционал — запуск функций и методов через некоторое время после запуска таймера.
Мы хотим создать универсальный класс, чтобы он мог работать с любыми классами и функциями. В частности, мы не можем позволить себе решить эту проблему наследованием от некоего класса Timered. Это неэстетично. Чтобы создать универсальный таймер необходимо нечто, что поможет его работать с объектами разных типов. То есть, делегат.
Что касается сигналов, это тоже средство, позволяющее уменьшить связность программы. Вместо того, чтобы прописывать в классах какие-то левые взаимосвязи, мы объявляем сигнал и позволяем любым сторонним классам к нему присоединиться.
Построение универсального интерфейса без применения наследования.
По сути мы оборачиваем объект и метод любого типа в обертку делегата. Дальше мы можем работать просто с делегатом, не особо задумываясь о многообразии типов.
Пример — класс таймеров. Функционал — запуск функций и методов через некоторое время после запуска таймера.
Мы хотим создать универсальный класс, чтобы он мог работать с любыми классами и функциями. В частности, мы не можем позволить себе решить эту проблему наследованием от некоего класса Timered. Это неэстетично. Чтобы создать универсальный таймер необходимо нечто, что поможет его работать с объектами разных типов. То есть, делегат.
Что касается сигналов, это тоже средство, позволяющее уменьшить связность программы. Вместо того, чтобы прописывать в классах какие-то левые взаимосвязи, мы объявляем сигнал и позволяем любым сторонним классам к нему присоединиться.
Хочется понять, много ли я потерял, реализовав эти же таймеры на C, используя callback на голые функции.
Ничего не потеряли. А некоторые (например, Линус Торвальдс) наоборот скажут, что Вы только выиграли реализовав таймеры на С :) С таким же успехом можно спросить: а что я потеряю реализовав таймеры с обратным вызовом на ASM? Просто разные парадигмы… В С для обратного вызова достаточно знать адрес функции (ну и желательно сигнатуру :), поэтому там смысла в таких обертках нет.
А вы не пробовали как-нибудь упростить синтаксис…
Неплохо, пока параметр 1.
Но, всё-таки…
delegate2<void, int, float, double, double> d3 = delegate2<void, int, float, double, double>::Method<A, &A::task3>(&a);
Это не гуд.
Неплохо, пока параметр 1.
Но, всё-таки…
delegate2<void, int, float, double, double> d3 = delegate2<void, int, float, double, double>::Method<A, &A::task3>(&a);
Это не гуд.
Мне кажетеся Вы меня путаете с коллегой neiver и вопрос адресован ему?
У меня нет идей как упростить синтаксис, насколько я понимаю С++ не сможет автоматически вывести типы шаблона на основании сигнатуры функции. Поэтому, можно разве что в правой части использовать auto для сокращения синтаксиса. Хотя возможно я ошибаюсь, я больше пишу на С# (там выведение типов для обобщений работает немного по другому), С++ я, увы, стал постепенно забывать.
У меня нет идей как упростить синтаксис, насколько я понимаю С++ не сможет автоматически вывести типы шаблона на основании сигнатуры функции. Поэтому, можно разве что в правой части использовать auto для сокращения синтаксиса. Хотя возможно я ошибаюсь, я больше пишу на С# (там выведение типов для обобщений работает немного по другому), С++ я, увы, стал постепенно забывать.
Еще такой момент: сугубо имхо, но идея перегрузки оператора != для sigslot — не самая лучшая, ибо ломается привычная логика — оператор неравенства используется для добавления. Интуитивно понять, что выражение
добавляет функцию в список вызова (а не выполняет проверку) невозможно.
В отличии от перегрузки += -=, использование которых мне кажется интуитивно понятным (хотя возможно мне это кажется понятным из-за знания С#)
somevar != foo();
добавляет функцию в список вызова (а не выполняет проверку) невозможно.
В отличии от перегрузки += -=, использование которых мне кажется интуитивно понятным (хотя возможно мне это кажется понятным из-за знания С#)
Комментарии (41)
RSS свернуть / развернуть