When I realized that the classes are getting really big and complicated, I splitted them into smaller classes. So the Actor class doesn't do everything anymore, but has some components that do the work instead.
A Common problem with this approach is, how do Compositors (in this case the Actor) and Components communicate? Some might say they shouldn't communicate at all, use something like an ECS etc. I looked at ECS et al., and they may be fine when you design your application around it, but I didn't.
Up to now I just friend'ed everything and called private methods, but that doesn't seem to be very elegant.
Another approach is some event/messaging system. So interested parties subscribe to interesting events, and other may trigger those events. There are many such event systems out there, but all seemed to be too heavy for me, so I made a minimal implementation of such a system.
I think this implementation has some Pro's and many Con's:
Pros
- Type safety
- Event function can have any signature even with return value. The `CallOne()` and `CallAll()` functions returns the same type as the called function (or a vector of it).
- You must define at compile time the signatures of functions your a going to call, and you can not call anything else. You get a compile error when a function signature is not found.
- A called function may be a `std::function` or Lambda.
- No inheritance needed.
- Single header file `events.h`, only ~110 lines.
- Minimal run time overhead.
Cons
- Function signature must be passed to each call of `Subscribe()`, `CallOne()` and `CallAll()`.
- You must define at compile time the signatures of functions your a going to call
- If you do anything wrong (e.g. wrong function signature), you get weird error messages
- Increased compile time, because a lot is done at compile time. But what you can do at compile time, doesn't have to be done at run time.
- You can not unsubscribe from events :(.
Examples
Lambda
sa::Events<
int(int, int)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
return i * j;
});
auto result = events.CallOne<int(int, int)>(1, 2, 3);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 6);
Lambda 2
sa::Events<
int(int, int)
> events;
auto func = [](int i, int j) -> int
{
return i + j;
};
events.Subscribe<int(int, int)>(2, func);
auto result = events.CallOne<int(int, int)>(2, 4, 5);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 9);
Event not found
When an event does not exist or nobody subscribed to the event it returns a default value, e.g. `0` for an `int`:
sa::Events<
int(int, int)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
return i * j;
});
auto result = events.CallOne<int(int, int)>(2, 2, 3);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 0);
Methods
class Foo
{
private:
sa::Events<
int(int, int)
> events;
int Bar(int i, int j)
{
return i * j;
}
public:
Foo()
{
events.Subscribe<int(int, int)>(1, std::bind(&Foo::Bar, this, std::placeholders::_1, std::placeholders::_2));
}
int DoBar(int i, int j)
{
return events.CallOne<int(int, int)>(1, i, j);
}
};
Foo foo;
auto result = foo.DoBar(3, 2);
assert(result == 6);
Different signatures
sa::Events<
int(int, int),
bool(int),
void(void)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
return i * j;
});
events.Subscribe<bool(int)>(2, [](int i) -> bool
{
return i != 0;
});
events.Subscribe<void(void)>(3, []()
{
std::count << "No arguments :(" << std::endl;
});
auto result = events.CallOne<int(int, int)>(1, 4, 5);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 20);
auto result2 = events.CallOne<bool(int)>(2, 5);
static_assert(std::is_same<decltype(result2), bool>::value);
assert(result2 == true);
// void
events.CallOne<void(void)>(3);
Multiple subscribers
sa::Events<
void(const std::string&)
> events;
events.Subscribe<void(const std::string&)>(1, [](const std::string& s)
{
std::cout << "Subscriber 1 " << s << std::endl;
});
events.Subscribe<void(const std::string&)>(1, [](const std::string& s)
{
std::cout << "Subscriber 2 " << s << std::endl;
});
events.CallAll<void(const std::string&)>(1, "Hello Subscribers!");
Subscriber 1 Hello Subscribers!
Subscriber 2 Hello Subscribers!
Conclusion
I was able to get rid of many virtual functions and the classes got lighter. But the usage is a bit cumbersome, because you always have to pass the function signature of the event as template argument, but you get type safety in return.
Download
Get it from the Github respository if you are interested (MIT).