V poslední době se stalo trendem programovat pomocí událostí. V C# tento mechanismus známe jako delegáty, v Javě pro změnu pod názvem listenery (listeners). C++ je jazyk o dost starší a takovýto mechanismus není nativní. To však neznamená, že něco takového není v C++ možné.
V tomhle článku si ukážeme, jak vytvořit mechanismus událostí podobný mechanismu delegátů v C#. Když se člověk zamyslí nad delegáty, zjistí, že to není nic jiného než datový typ reprezentující funkci, tedy něco jako ukazatele na funkci. Z principu bychom mohli říci, že v C++ stačí předat třídě, která událost vyvolala, ukazatel na funkci, která událost zpracuje. Vezměme si následující modelovou situaci auto-dveře. Představme si, že máme objekt auto, jehož součástí jsou čtyři objekty reprezentující dveře. V naší modelové situaci bude docházet k události, když se jednotlivé dveře otevřou. Nazvěme to událostí DoorOpen. Používat by se měla co nejjednodušeji, například takhle:
class Car {
...
Door m_LeftFrontDoor; //clen triedy Car reprezentujuci lave predne dvere
...
Car() {
...
//v konstruktore povieme, akou funkciou ma byt
// spracovavana udalost onDoorOpen pre lave predne dvere
m_LeftFrontDoor.setDoorOpenEvent(new DoorOpenEvent(Car::onDoorOpen));
...
}
//tato funkcia sa vykona v momente, ked bude vyvolana udalost
onDoorOpen() {
...
}
};
Jak byste to realizovali? Popřemýšlejte. Většinu lidí hned napadne v třídě Door nadefinovat proměnnou, která bude obsahovat adresu funkce. V podstatě to je logický postup, problém však je, že C++ vám nedovolí získat adresu členské funkce třídy jen tak lehko. O tom se můžete přesvědčit v následujícím příkladě.
#include <iostream>
using namespace std;
class A {
public:
void test() {
cout << "called A::test()" << endl;
}
}
void invoke(void(*func)(void))
{
func();
}
int main()
{
A class_a;
invoke(class_a.test);
}
Program má za úkol získat adresu funkce test() a zavolat ji. Události jsou vlastně o tom, aby se zavolala daná funkce. Jak jste však zjistili, program nejde zkompilovat. Kdyby funkce test() byla statická, situace by se změnila a program by se rozběhl. Problém je, že statická funkce není to pravé ořechové. Jak tedy získat adresu funkce? Zkuste si program upravit následujícím způsobem:
#include <iostream>
using namespace std;
class A {
public:
void test() {
cout << "called A::test()" << endl;
}
}
void invoke(A* class_a, void(A::*func)(void))
{
(class_a->*func)();
}
int main()
{
A class_a;
invoke(class_a, &A::test);
}
Nyní už kompilace proběhla. Nic se nezměnilo, jen způsob zápisu ukazatele na členskou funkci a volání invoke(). Mimo ukazatele na členskou funkci potřebujeme i ukazatel na instanci třídy, pro kterou tuto funkci voláme.
Už máme něco, na čem můžeme postavit náš mechanismus událostí. Dalším krokem je doplnění členské proměnné do třídy Door, jejímž obsahem bude adresa funkce v třídě Car. Jenže jak to realizovat, když třída Car se nachází až za třídou Door? K tomuto účelu si vytvoříme abstraktní třídu s jednou virtuální funkcí invoke(). Je to jakýsi mezičlánek, pomocí něhož třída Door zavolá tu správnou funkci. Ilustračně to bude vypadat následovně:
class Event
{
virtual void invoke() = 0;
};
//Ukazka triedy, ktora udalost bude volat.
//Volanie sa vykonava skrz triedu Event.
class Door {
...
Event* m_DoorOpenEvent;
void open {
...
m_DoorOpenEvent->invoke();
...
}
};
class Car {
Door m_LeftFrontDoor;
...
//trieda, pomocou ktorej zavolame Car::onDoorOpen()
class DoorOpenEvent : public Event {
private:
Car* m_Obj; //obsahuje ukazovatel na instanciu auta
void(Car::* m_Func)(); //obsahuje ukazovatel na funkciu, ktora sa bude volat pri vyvolani udalosti
public:
DoorOpenEvent(Car* object, void(Car::* func)()) {
m_Obj = object;
m_Func = func;
}
virtual void invoke() {
(m_Obj->*m_Func)();
}
}
...
Car::Car()
{
//zaregistrovanie udalosti. Bude volana funkcia Car::onDoorOpen
m_LeftFrontDoor.m_DoorOpenEvent = new DoorOpenEvent(this, &Car::onDoorOpen);
}
//funkcia, ktora sa zavola v momente, ked je vyvolana udalost
void Car::onDoorOpen() {
...
}
};
Předem upozorňuji, že výše uvedený kód nelze zkompilovat a že je pouze ilustrační. Nechci zdrojový kód znečišťovat dalším kódem, ale chci, aby obsahoval jen to nejpodstatnější. Nejprve si všimněte třídy Event. Tato třída je abstraktní. Samotná třída nic nevykonává. Je však důležitá pro další třídu DoorOpenEvent. Skrze tuto třídu budeme volat příslušnou funkci. U této třídy se plně využívá dědičnosti a polymorfismu, když se vykoná invoke() děděné třídy.
Třída Door je nejjednodušší. Obsahuje členskou proměnnou ukazující na Event objekt. Mimo to se zde nachází funkce open(), která má za úkol vyvolat událost. Zajímavější je však třída Car. Už na začátku jsme si řekli, že obsahuje objekty dveří. V tomto případě se jedná o členskou proměnnou m_LeftFrontDoor. Další jednoduchou záležitostí je funkce onOpenDoor(), kterou chceme volat v momentě, kdy je vyvolaná událost.
Nyní k nejzajímavější části. Ve třídě Car se nachází vnořená třída DoorOpenEvent. Tato třída je děděná od třídy Event a má přetíženou funkci invoke(). Kromě toho má třída dvě členské proměnné. Proměnná m_Obj ukazuje na konkrétní instanci Car a proměnná m_Func ukazuje na funkci, kterou chceme zavolat. Tyto proměnné se následně použijí ve funkci invoke() k tomu, abychom mohli zavolat správnou funkci. Konstruktor třídy CarEvent pouze nastaví tuto dvojici proměnných.
Použití třídy DoorOpenEvent můžete vidět v konstruktoru třídy Car, kde se pro objekt m_LeftFrontDoor a událost m_DoorOpenEvent zaregistruje volání funkce Car::onDoorOpen().
Toto je pouze princip, na kterém si události postavíme. Praktický příklad uvádím až nyní, kdy by mechanismus měl být víceméně jasný. Kromě toho jsem následující příklad obohatil o argumenty události. V tomto případě budeme pomocí argumentu předávat fiktivní informaci o tom, jakým způsobem byly dveře na autě otevřeny. Jestli klíčem (KEY) nebo pomocí centrálního uzamykání (DISTANCE_CONTROL). Ve funkci main() nakonec zavoláme funkci openWithKey() pro přední levé dveře a funkci openWithDistanceControl() pro zadní pravé dveře.
events.h
#if !defined(_EVENTS_H_)
#define _EVENTS_H_
class EventArgs {
public:
void* m_Sender;
};
class Event
{
public:
virtual void invoke(EventArgs* args) = 0;
};
#endif
Do souboru events.h jsem umístil všechny společné prvky události. Kromě už výše popsané abstraktní třídy Event jsem ještě doplnil základní třídu EventArgs, která slouží na předávání argumentů událostem.
door.h
#if !defined(_DOOR_H_)
#define _DOOR_H_
#include "events.h"
#include <iostream>
using namespace std;
enum lock_t {
DISTANCE_CONTROL,
KEY
};
enum door_t {
LEFT_FRONT,
LEFT_BACK,
RIGHT_FRONT,
RIGHT_BACK
};
string doorTypeToStr(door_t doorType);
class DoorOpenEventArgs : public EventArgs {
public:
DoorOpenEventArgs(void* sender, lock_t lockType);
public:
lock_t m_LockType;
};
class Door {
public:
void setOpenDoorEvent(Event* event);
void openWithKey();
void openWithDistanceControl();
private:
auto_ptr<Event> m_OpenDoorEvent;
public:
door_t m_DoorType;
};
#endif
door.cc
#include "door.h"
string doorTypeToStr(door_t doorType)
{
string out;
switch (doorType) {
case LEFT_FRONT:
out = "lave predne dvere";
break;
case LEFT_BACK:
out = "lave zadne dvere";
break;
case RIGHT_FRONT:
out = "prave predne dvere";
break;
case RIGHT_BACK:
out = "prave zadne dvere";
break;
}
return out;
}
DoorOpenEventArgs::DoorOpenEventArgs(void* sender, lock_t lockType)
{
m_Sender = sender;
m_LockType = lockType;
}
void Door::setOpenDoorEvent(Event* event)
{
m_OpenDoorEvent.reset(event);
}
void Door::openWithKey()
{
if (m_OpenDoorEvent.get()) {
DoorOpenEventArgs open_arg(this, KEY);
m_OpenDoorEvent->invoke(&open_arg);
}
}
void Door::openWithDistanceControl()
{
if (m_OpenDoorEvent.get()) {
DoorOpenEventArgs open_arg(this, DISTANCE_CONTROL);
m_OpenDoorEvent->invoke(&open_arg);
}
}
Tady jsem umístil dvojici enumerací. První lock_t, která definuje způsoby otevření dveří, a door_T, která udává typ dveří. Kromě toho je tu funkce doorTypeToStr() pro převod enumerace door_t na text. To však z pohledu události je pouze doplňkem, na samotný mechanismus se to nevztahuje. Pro mechanismus události je důležitější třída DoorOpenEventArgs, která je děděná z třídy EventArgs a obohacená o členskou proměnnou m_LockType. Instance této třídy se bude předávat voláním události a bude obsahovat potřebné atgumenty. Třída Door je obohacená o členskou proměnnou m_DoorType a m_OpenDoorEvent je realizovaná pomocí std::auto_ptr. Tím se zabezpečí správné uvolnění třídy Event, aby nedocházelo ke zbytečným únikům paměti.
car.h
#if !defined(_CAR_H_)
#define _CAR_H_
#include "door.h"
class Car {
private:
class DoorOpenEvent : public Event
{
public:
DoorOpenEvent(Car* object, void(Car::* func)(DoorOpenEventArgs*));
virtual void invoke(EventArgs* args);
private:
Car* m_Obj;
void(Car::* m_Func)(DoorOpenEventArgs* args);
};
public:
Car();
void onOpening(DoorOpenEventArgs* args);
Door m_LeftFront;
Door m_LeftBack;
Door m_RightFront;
Door m_RightBack;
};
#endif
car.cc
#include "car.h"
Car::DoorOpenEvent::DoorOpenEvent(Car* object, void(Car::* func)(DoorOpenEventArgs*))
{
m_Obj = object;
m_Func = func;
}
void Car::DoorOpenEvent::invoke(EventArgs* args)
{
(m_Obj->*m_Func)((DoorOpenEventArgs*)args);
}
Car::Car()
{
m_LeftFront.m_DoorType = LEFT_FRONT;
m_LeftFront.setOpenDoorEvent( new DoorOpenEvent(this, &Car::onOpening) );
m_LeftBack.m_DoorType = LEFT_BACK;
m_LeftBack.setOpenDoorEvent( new DoorOpenEvent(this, &Car::onOpening) );
m_RightFront.m_DoorType = RIGHT_FRONT;
m_RightFront.setOpenDoorEvent( new DoorOpenEvent(this, &Car::onOpening) );
m_RightBack.m_DoorType = RIGHT_BACK;
m_RightBack.setOpenDoorEvent( new DoorOpenEvent(this, &Car::onOpening) );
}
void Car::onOpening(DoorOpenEventArgs* args)
{
Door* door = (Door*)args->m_Sender;
switch (args->m_LockType) {
case DISTANCE_CONTROL:
cout << doorTypeToStr(door->m_DoorType) << " boli otvorene pomocou central. uzamikania" << endl;
break;
case KEY:
cout << doorTypeToStr(door->m_DoorType) << " boli otvorene pomocou kluca" << endl;
break;
}
}
Třída Car obsahuje vnořenou třídu DoorOpenEvent, která je doplněná o argumenty. Kromě toho má třída Car čtyři členské proměnné reprezentující jednotlivé dveře auta. Jejich instance a registrace událostí je realizovaná v konstruktoru Car::Car().
main.cc
#include <iostream>
#include "car.h"
using namespace std;
int main()
{
Car car;
car.m_RightFront.openWithKey();
car.m_LeftBack.openWithDistanceControl();
return 0;
}
Program po spuštění vytvoří objekt car, následně zrealizuje otevření dveří pomocí klíče a centrálního uzamykání. To vyvolá událost DoorOpenEvent a vykoná se obsah funkce Car::onOpening() pro každé otevření dveří.
Tento příklad je, co se funkčnosti týká, celkem jednoduchý. Z pohledu psaní kódu to je relativně slušný počet řádků. Tento stav se zhorší přidáním dalších nových událostí, kvůli čemuž začne být kód postupně méně přehledný. To se však dá elegantně vyřešit pomocí šablon, s jejichž pomocí drasticky snížíme počet řádků kódu. Zvýšíme tak přehlednost kódu a vytvoříme znovupoužitelný kód. Nebudu se tu rozepisovat o tom, jak psát šablony. Uvedu pouze rozsah změn, které si to vyžaduje, a také příklad použití.
Nejprve do events.h umístíme šablonu naší vnořené funkce.
#if !defined(_EVENTS_H_)
#define _EVENTS_H_
class EventArgs {
public:
void* m_Sender;
};
class Event
{
public:
virtual void invoke(EventArgs* args) = 0;
};
template<class T, class ARG> class EventHandler : public Event
{
public:
EventHandler(T* obj, void(T::* func)(ARG*)) {
m_Obj = obj;
m_Func = func;
}
virtual void invoke(EventArgs* args) {
(m_Obj->*m_Func)((ARG*)args);
}
private:
T* m_Obj;
void (T::* m_Func)(ARG*);
};
#endif
Docílili jsme toho, že už nebudeme muset vytvářet vnořené třídy pro každou novou událost. Třída Door zůstává nezměněná. Co se však změní, je třída Car. Úplně se vypustí vnořená třída a změní se registrace události, čímž získáme mnohem přehlednější kód.
car.h
#if !defined(_CAR_H_)
#define _CAR_H_
#include "door.h"
class Car {
public:
Car();
void onOpening(DoorOpenEventArgs* args);
Door m_LeftFront;
Door m_LeftBack;
Door m_RightFront;
Door m_RightBack;
};
#endif
car.cc
#include "car.h"
Car::Car()
{
m_LeftFront.m_DoorType = LEFT_FRONT;
m_LeftFront.setOpenDoorEvent( new EventHandler<Car, DoorOpenEventArgs>(this, &Car::onOpening) );
m_LeftBack.m_DoorType = LEFT_BACK;
m_LeftBack.setOpenDoorEvent( new EventHandler<Car, DoorOpenEventArgs>(this, &Car::onOpening) );
m_RightFront.m_DoorType = RIGHT_FRONT;
m_RightFront.setOpenDoorEvent( new EventHandler<Car, DoorOpenEventArgs>(this, &Car::onOpening) );
m_RightBack.m_DoorType = RIGHT_BACK;
m_RightBack.setOpenDoorEvent( new EventHandler<Car, DoorOpenEventArgs>(this, &Car::onOpening) );
}
void Car::onOpening(DoorOpenEventArgs* args)
{
Door* door = (Door*)args->m_Sender;
switch (args->m_LockType) {
case DISTANCE_CONTROL:
cout << doorTypeToStr(door->m_DoorType) << " boli otvorene pomocou central. uzamikania" << endl;
break;
case KEY:
cout << doorTypeToStr(door->m_DoorType) << " boli otvorene pomocou kluca" << endl;
break;
}
}
Takto jsme získali znovupoužitelnost a jednoduchost událostí. Jediné, co potřebujeme udělat, je událost zaregistrovat pomocí šablony EventHandler. Tím pádem se z events.h stane poměrně zajímavý soubor, který můžeme přenášet z aplikace do aplikace, z projektu do projektu. Jestli jste ještě pohodlnější, můžete vytvořit makro pro každý EventArgs.
#define NEW_OPEN_EVENT(cls, func) new EventHandler(this, &cls::func)
...
m_RightFront.setOpenDoorEvent(NEW_OPEN_EVENT(Car, onOpening));
Určitě se najde ještě množství vylepšení. Cílem však bylo objasnit mechanismus událostí založených na principu delegátů v C#.