Возможно ли реализовать события на С++?
Я хотел реализовать событие С# на С++, чтобы увидеть, могу ли я это сделать. Я застрял, я знаю, что дно ошибочно, но я понимаю, что моя самая большая проблема - это...
Как я могу перегрузить оператор ()
, который находится в T
, в данном случае int func(float)
? Я не могу? Могу я? Могу ли я реализовать хорошую альтернативу?
#include <deque>
using namespace std;
typedef int(*MyFunc)(float);
template<class T>
class MyEvent
{
deque<T> ls;
public:
MyEvent& operator +=(T t)
{
ls.push_back(t);
return *this;
}
};
static int test(float f){return (int)f; }
int main(){
MyEvent<MyFunc> e;
e += test;
}
Ответы
Ответ 1
Если вы можете использовать Boost, рассмотрите возможность использования Boost.Signals2, который обеспечивает функциональность сигналов-слотов/событий/наблюдателей. Он прост и прост в использовании и довольно гибкий. Boost.Signals2 также позволяет вам регистрировать произвольные вызываемые объекты (например, функторы или связанные функции-члены), поэтому он более гибкий и имеет множество функций, которые помогут вам правильно управлять сроками жизни.
Если вы пытаетесь реализовать его самостоятельно, вы на правильном пути. У вас есть проблема, хотя: что именно вы хотите делать со значениями, возвращаемыми из каждой зарегистрированной функции? Вы можете вернуть только одно значение из operator()
, поэтому вам нужно решить, хотите ли вы ничего не возвращать, или один из результатов, или каким-либо образом обобщить результаты.
Предполагая, что мы хотим игнорировать результаты, довольно просто реализовать это, но это немного проще, если вы берете каждый из типов параметров в качестве отдельного параметра типа шаблона (в качестве альтернативы вы можете использовать что-то вроде Boost.TypeTraits, которое позволяет легко анализировать тип функции):
template <typename TArg0>
class MyEvent
{
typedef void(*FuncPtr)(TArg0);
typedef std::deque<FuncPtr> FuncPtrSeq;
FuncPtrSeq ls;
public:
MyEvent& operator +=(FuncPtr f)
{
ls.push_back(f);
return *this;
}
void operator()(TArg0 x)
{
for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it)
(*it)(x);
}
};
Для этого требуется, чтобы зарегистрированная функция имела тип возврата void
. Чтобы иметь возможность принимать функции с любым типом возврата, вы можете изменить FuncPtr
на
typedef std::function<void(TArg0)> FuncPtr;
(или используйте boost::function
или std::tr1::function
, если у вас нет версии С++ 0x). Если вы хотите что-то сделать с возвращаемыми значениями, вы можете взять возвращаемый тип в качестве другого параметра шаблона в MyEvent
. Это должно быть относительно просто.
С приведенной выше реализацией должно работать следующее:
void test(float) { }
int main()
{
MyEvent<float> e;
e += test;
e(42);
}
Другим подходом, который позволяет поддерживать разные типы событий, было бы использовать параметр одного типа для типа функции и иметь несколько перегруженных перегрузок operator()
, каждый из которых принимает различное количество аргументов. Эти перегрузки должны быть шаблонами, иначе вы получите ошибки компиляции для любой перегрузки, не соответствующей действительности arity события. Вот пример:
template <typename TFunc>
class MyEvent
{
typedef typename std::add_pointer<TFunc>::type FuncPtr;
typedef std::deque<FuncPtr> FuncPtrSeq;
FuncPtrSeq ls;
public:
MyEvent& operator +=(FuncPtr f)
{
ls.push_back(f);
return *this;
}
template <typename TArg0>
void operator()(TArg0 a1)
{
for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it)
(*it)(a1);
}
template <typename TArg0, typename TArg1>
void operator()(const TArg0& a1, const TArg1& a2)
{
for (typename FuncPtrSeq::iterator it(ls.begin()); it != ls.end(); ++it)
(*it)(a1, a2);
}
};
(я использовал std::add_pointer
из С++ 0x здесь, но модификатор этого типа можно также найти в Boost и С++ TR1, он просто немного чище использует шаблон функции, так как вы можете использовать функцию тип напрямую, вам не нужно использовать тип указателя функции.) Здесь приведен пример использования:
void test1(float) { }
void test2(float, float) { }
int main()
{
MyEvent<void(float)> e1;
e1 += test1;
e1(42);
MyEvent<void(float, float)> e2;
e2 += test2;
e2(42, 42);
}
Ответ 2
Вы абсолютно можете. Джеймс Макнеллис уже связан с полным решением, но для вашего примера игрушек мы можем сделать следующее:
#include <deque>
using namespace std;
typedef int(*MyFunc)(float);
template<typename F>
class MyEvent;
template<class R, class Arg>
class MyEvent<R(*)(Arg)>
{
typedef R (*FuncType)(Arg);
deque<FuncType> ls;
public:
MyEvent<FuncType>& operator+=(FuncType t)
{
ls.push_back(t);
return *this;
}
void operator()(Arg arg)
{
typename deque<FuncType>::iterator i = ls.begin();
typename deque<FuncType>::iterator e = ls.end();
for(; i != e; ++i) {
(*i)(arg);
}
}
};
static int test(float f){return (int)f; }
int main(){
MyEvent<MyFunc> e;
e += test;
e(2.0);
}
Здесь я использовал частичную специализацию, чтобы разделить компоненты типа указателя функции, чтобы обнаружить тип аргумента. boost.signals делает это и многое другое, используя такие функции, как стирание стилей и черты, чтобы определить эту информацию для неработающих указателей, типизированных вызываемых объектов.
Для N аргументов существует два подхода. "Легкий" способ, который был добавлен для С++ 0x, заключается в использовании вариативных шаблонов и нескольких других функций. Однако мы делали это с тех пор, пока эти функции не были добавлены, и я не знаю, какие компиляторы, если они есть, поддерживайте вариационные шаблоны, поэтому мы можем сделать это сложным способом, который, опять-таки, специализируется:
template<typename R, typename Arg0, typename Arg1>
class MyEvent<R(*)(Arg0, Arg1)>
{
typedef R (*FuncType)(Arg0, Arg1);
deque<FuncType> ls;
...
void operatror()(Arg0 a, Arg1)
{ ... }
MyEvent<FuncType>& operator+=(FuncType f)
{ ls.push_back(f); }
...
};
Конечно, это утомительно, поэтому есть библиотеки, такие как boost.signals, которые уже удалили его (и те используют макросы и т.д., чтобы облегчить некоторые из скуки).
Чтобы разрешить синтаксис стиля MyEvent<int, int>
, вы можете использовать следующий метод
struct NullEvent;
template<typename A = NullEvent, typename B = NullEvent, typename C = NullEvent>
class HisEvent;
template<>
struct HisEvent<NullEvent,NullEvent,NullEvent>
{ void operator()() {} };
template<typename A>
struct HisEvent<A,NullEvent,NullEvent>
{ void operator()(A a) {} };
template<typename A, typename B>
struct HisEvent<A, B, NullEvent>
{
void operator()(A a, B b) {}
};
template<typename A, typename B, typename C>
struct HisEvent
{
void operator()(A a, B b, C c)
{}
};
static int test(float f){return (int)f; }
int main(){
MyEvent<MyFunc> e;
e += test;
e(2.0);
HisEvent<int> h;
HisEvent<int, int> h2;
}
Тип NullEvent
используется в качестве заполнителя, и мы снова используем частичную специализацию для определения арности.
Ответ 3
Это возможно, но не с вашим текущим дизайном. Проблема заключается в том, что сигнатура функции обратного вызова заблокирована в аргументе шаблона. Я не думаю, что вы должны пытаться поддерживать это в любом случае, все обратные вызовы в том же списке должны иметь одну и ту же подпись, не так ли?
Ответ 4
EDIT: добавлена надежная реализация потока, основанная на этом ответе. Многие исправления и улучшения производительности
Это моя версия, улучшающая функцию Джеймса Макнеллиса путем добавления: operator-=
, вариационного шаблона для поддержки любого сорта сохраненных вызываемых объектов, методов удобства Bind(func, object)
и Unbind(func, object)
для простого привязки объектов и функций-членов экземпляра, операторов присваивания и сравнения с nullptr
. Я отказался от использования std::add_pointer
, чтобы просто использовать std::function
, который в моих попытках более гибкий (принимает как lambdas, так и std:: function). Кроме того, я перешел к использованию std::vector
для более быстрой итерации и удаления возвращаемых *this
в операторы, так как в любом случае он не выглядит очень безопасным/полезным. Все еще отсутствует семантика С#: события С# не могут быть удалены из-за пределов класса, в котором они объявлены (было бы легко добавить это по дружбе государства к шаблонизированному типу).
Это следует за кодом, обратная связь приветствуется:
#pragma once
#include <typeinfo>
#include <functional>
#include <stdexcept>
#include <memory>
#include <atomic>
#include <cstring>
template <typename TFunc>
class Event;
template <class RetType, class... Args>
class Event<RetType(Args ...)> final
{
private:
typedef typename std::function<RetType(Args ...)> Closure;
struct ComparableClosure
{
Closure Callable;
void *Object;
uint8_t *Functor;
int FunctorSize;
ComparableClosure(const ComparableClosure &) = delete;
ComparableClosure() : Object(nullptr), Functor(nullptr), FunctorSize(0) { }
ComparableClosure(Closure &&closure) : Callable(std::move(closure)), Object(nullptr), Functor(nullptr), FunctorSize(0) { }
~ComparableClosure()
{
if (Functor != nullptr)
delete[] Functor;
}
ComparableClosure & operator=(const ComparableClosure &closure)
{
Callable = closure.Callable;
Object = closure.Object;
FunctorSize = closure.FunctorSize;
if (closure.FunctorSize == 0)
{
Functor = nullptr;
}
else
{
Functor = new uint8_t[closure.FunctorSize];
std::memcpy(Functor, closure.Functor, closure.FunctorSize);
}
return *this;
}
bool operator==(const ComparableClosure &closure)
{
if (Object == nullptr && closure.Object == nullptr)
{
return Callable.target_type() == closure.Callable.target_type();
}
else
{
return Object == closure.Object && FunctorSize == closure.FunctorSize
&& std::memcmp(Functor, closure.Functor, FunctorSize) == 0;
}
}
};
struct ClosureList
{
ComparableClosure *Closures;
int Count;
ClosureList(ComparableClosure *closures, int count)
{
Closures = closures;
Count = count;
}
~ClosureList()
{
delete[] Closures;
}
};
typedef std::shared_ptr<ClosureList> ClosureListPtr;
private:
ClosureListPtr m_events;
private:
bool addClosure(const ComparableClosure &closure)
{
auto events = std::atomic_load(&m_events);
int count;
ComparableClosure *closures;
if (events == nullptr)
{
count = 0;
closures = nullptr;
}
else
{
count = events->Count;
closures = events->Closures;
}
auto newCount = count + 1;
auto newClosures = new ComparableClosure[newCount];
if (count != 0)
{
for (int i = 0; i < count; i++)
newClosures[i] = closures[i];
}
newClosures[count] = closure;
auto newEvents = ClosureListPtr(new ClosureList(newClosures, newCount));
if (std::atomic_compare_exchange_weak(&m_events, &events, newEvents))
return true;
return false;
}
bool removeClosure(const ComparableClosure &closure)
{
auto events = std::atomic_load(&m_events);
if (events == nullptr)
return true;
int index = -1;
auto count = events->Count;
auto closures = events->Closures;
for (int i = 0; i < count; i++)
{
if (closures[i] == closure)
{
index = i;
break;
}
}
if (index == -1)
return true;
auto newCount = count - 1;
ClosureListPtr newEvents;
if (newCount == 0)
{
newEvents = nullptr;
}
else
{
auto newClosures = new ComparableClosure[newCount];
for (int i = 0; i < index; i++)
newClosures[i] = closures[i];
for (int i = index + 1; i < count; i++)
newClosures[i - 1] = closures[i];
newEvents = ClosureListPtr(new ClosureList(newClosures, newCount));
}
if (std::atomic_compare_exchange_weak(&m_events, &events, newEvents))
return true;
return false;
}
public:
Event()
{
std::atomic_store(&m_events, ClosureListPtr());
}
Event(const Event &event)
{
std::atomic_store(&m_events, std::atomic_load(&event.m_events));
}
~Event()
{
(*this) = nullptr;
}
void operator =(const Event &event)
{
std::atomic_store(&m_events, std::atomic_load(&event.m_events));
}
void operator=(nullptr_t nullpointer)
{
while (true)
{
auto events = std::atomic_load(&m_events);
if (!std::atomic_compare_exchange_weak(&m_events, &events, ClosureListPtr()))
continue;
break;
}
}
bool operator==(nullptr_t nullpointer)
{
auto events = std::atomic_load(&m_events);
return events == nullptr;
}
bool operator!=(nullptr_t nullpointer)
{
auto events = std::atomic_load(&m_events);
return events != nullptr;
}
void operator +=(Closure f)
{
ComparableClosure closure(std::move(f));
while (true)
{
if (addClosure(closure))
break;
}
}
void operator -=(Closure f)
{
ComparableClosure closure(std::move(f));
while (true)
{
if (removeClosure(closure))
break;
}
}
template <typename TObject>
void Bind(RetType(TObject::*function)(Args...), TObject *object)
{
ComparableClosure closure;
closure.Callable = [object, function](Args&&...args)
{
return (object->*function)(std::forward<Args>(args)...);
};
closure.FunctorSize = sizeof(function);
closure.Functor = new uint8_t[closure.FunctorSize];
std::memcpy(closure.Functor, (void*)&function, sizeof(function));
closure.Object = object;
while (true)
{
if (addClosure(closure))
break;
}
}
template <typename TObject>
void Unbind(RetType(TObject::*function)(Args...), TObject *object)
{
ComparableClosure closure;
closure.FunctorSize = sizeof(function);
closure.Functor = new uint8_t[closure.FunctorSize];
std::memcpy(closure.Functor, (void*)&function, sizeof(function));
closure.Object = object;
while (true)
{
if (removeClosure(closure))
break;
}
}
void operator()()
{
auto events = std::atomic_load(&m_events);
if (events == nullptr)
return;
auto count = events->Count;
auto closures = events->Closures;
for (int i = 0; i < count; i++)
closures[i].Callable();
}
template <typename TArg0, typename ...Args2>
void operator()(TArg0 a1, Args2... tail)
{
auto events = std::atomic_load(&m_events);
if (events == nullptr)
return;
auto count = events->Count;
auto closures = events->Closures;
for (int i = 0; i < count; i++)
closures[i].Callable(a1, tail...);
}
};
Я тестировал это с помощью этого:
#include <iostream>
using namespace std;
class Test
{
public:
void foo() { cout << "Test::foo()" << endl; }
void foo1(int arg1, double arg2) { cout << "Test::foo1(" << arg1 << ", " << arg2 << ") " << endl; }
};
class Test2
{
public:
Event<void()> Event1;
Event<void(int, double)> Event2;
void foo() { cout << "Test2::foo()" << endl; }
Test2()
{
Event1.Bind(&Test2::foo, this);
}
void foo2()
{
Event1();
Event2(1, 2.2);
}
~Test2()
{
Event1.Unbind(&Test2::foo, this);
}
};
int main(int argc, char* argv[])
{
(void)argc;
(void)argv;
Test2 t2;
Test t1;
t2.Event1.Bind(&Test::foo, &t1);
t2.Event2 += [](int arg1, double arg2) { cout << "Lambda(" << arg1 << ", " << arg2 << ") " << endl; };
t2.Event2.Bind(&Test::foo1, &t1);
t2.Event2.Unbind(&Test::foo1, &t1);
function<void(int, double)> stdfunction = [](int arg1, double arg2) { cout << "stdfunction(" << arg1 << ", " << arg2 << ") " << endl; };
t2.Event2 += stdfunction;
t2.Event2 -= stdfunction;
t2.foo2();
t2.Event2 = nullptr;
}