Collection of experiments on "events" in C++.
The folder examples/observable
contains a small program featuring a basic
implementation of the Observer design pattern.
For more information of this design pattern, see the Observer page on refactoring.guru.
The pubsub.h
header file defines two class templates, Publisher
and Subscriber
,
that can be used to quickly implement an Observer design pattern.
Theses classes can help reduce code duplication when the design pattern is implemented at multiple locations in the code base. They also offer a more systematic lifecycle management with automatic unsubscribing when either the publisher or the subscriber is destroyed.
Definition of both a "publisher" and a "subscriber" class is facilitated.
Publisher:
class MyPublisher : public Publisher<MySubscriber>
{
public:
void greets();
};
Subscriber:
class MySubscriber : public Subscriber<MyPublisher>
{
public:
explicit MySubscriber(MyPublisher* pub = nullptr);
virtual void sayHello() = 0;
};
Definition of greets()
:
void MyPublisher::greets()
{
notify(&MySubscriber::sayHello);
}
Usage (assuming GermanSubscriber
is derived from MySubscriber
):
MyPublisher pub;
GermanSubscriber sub;
pub.addSubscriber(&sub);
pub.greets(); // prints "Guten tag!", probably
An EventEmitter
class inspired by Node.js class EventEmitter.
The idea here is to allow emitting and listening to events without prior event registration.
While the above methods require the definition of various classes describing the sets
of events that can happen, the EventEmitter
can be used as-is and supports any event.
The reduces the amount of code that needs to be written ! 😄
The class can be used both using inheritance or as a member of a class.
Example (as a member):
class Person
{
public:
EventEmitter events;
private:
std::string m_name = "John Doe";
public:
const std::string& name() const { return m_name; }
void setName(std::string n)
{
if (n != m_name)
{
m_name = std::move(n);
nameChanged(m_name);
}
}
void nameChanged(const std::string& name)
{
events.emit(&Person::nameChanged, name);
}
};
Usage:
int main() {
Person p;
p.events.on(&Person::nameChanged, [](std::string n) {
std::cout << "Hello " << n << "!" << std::endl;
});
p.setName("Homer Simpson");
}
The EventEmitter
class supports partial use of the signal's parameters, so the following
also works:
Person p;
p.on(&Person::nameChanged, [&p](/* std::string */) {
std::cout << "Hello " << p.name() << "!" << std::endl;
});
p.setName("Homer Simpson");
A class, based on EventEmitter
, that provides a connection mechanism between signals and slots
similar to Qt's QObject.
Users are meant to derive from Object
and use connect()
for connection a signal
to a slot and emit()
for emitting signals.
Using this class could be considered safer than using EventEmitter
directly as:
- the set of "events" ought to be restricted to the "signals" declared in the object class (reducing the potential for surprises);
- disconnection is done automatically when either object destroyed (as opposed to only when the emitter is destroyed).
Example:
class Button : public Object
{
public:
void clicked()
{
emit(&Button::clicked);
}
};
class Dialog : public Object
{
private:
bool m_visible = false;
public:
bool visible() const { return m_visible; }
void open() { m_visible = true; opened(); }
void opened() { emit(&Dialog::opened); }
};
int main()
{
Button button;
Dialog dialog;
Object::connect(&button, &Button::clicked, &dialog, &Dialog::open);
Object::connect(&dialog, &Dialog::opened, []() {
std::cout << "Dialog opened!" << std::endl;
});
button.clicked();
}
Thread-safety: 🧶
Code is NOT thread-safe ; was designed to be used in a single-threaded environment.
Requirements:
- a compiler with C++17 support
- CMake
Step-by-step build instructions:
Create a build directory:
mkdir build && cd build
Generate the project:
cmake ..
Build (linux):
make
Build (Windows):
cmake --build . --config Release --target ALL_BUILD