My question regarding design is: should event handlers have a return value, or should it send back the "return value" in the form of an event, itself?
If you are going "general purpose", you have no choice but to support the possibility of "returning" a value.
Forcing a situation where event triggers event is unnecessarily convoluted when a given event is packaging the result of a possibly related task-oriented event.
Consider what happens in such cases from the client perspective without native support for "returning" a value: the client needs to register an event handler, trigger the event, wait on the result, package the result from within the even handler when the event is handled, flag the result as ready within the event handler, deregister the event handler, and release resources related to the event and its synchronization.
I am aware you said "asynchronous". That's exactly my point in laying out the above situation. Arbitrary handling of asynchronous events is a fine goal, but a "general purpose" system must necessarily also be capable of expressing "package" events from client perspective.
A task-oriented event would be one like "Load $(Filename) Into $(std::string)". From client view, this is a perfectly normal opportunity for performance improvements. (This improvement would be not waiting on the read operation to complete from the same code that waits on connections.) Consider a simple HTTP server: the client registers an event handler, the client triggers such an event by packing the associated socket into a `client_data' pointer or similar, the event handler when triggered delivers the file, the event handler releases associated resources, the client code is free to continue waiting (listening) on connections unrelated to what the event handler is or is not doing.
Such an event is not necessarily a "package" event. To continue, the "package" event would be the result of the operation related to an event. So, from client perspective the "Load $(Filename) Into $(std::string)" as a "package" event necessarily "returns" a `std::string' variable.
Code:
std::string sFile(SendEvent<std::string>(sSystem, LOAD_FILE, "somefile.txt"));
The relationship between the two is purely resolved in the flow of client code. It isn't really a mechanical issue in any way.
It is, I'd argue, a necessary evil to build such a facility into an event system in order to call it "general purpose".
While the former is easier to work with, the former can be implemented as basis of the second.
Assuming asynchronous behavior, with appropriate constructs you can "go in either direction" to get the other mechanic.
This is definitely true if there can only be one type of event sent as a reply (which is equal to the return type method which can have ony one type), but it does get a bit less clear if there can be more than one type of event (such as SuccessEvent and FailureEvent).
It is not less clear. It is a part of the API.
It is exactly the same as the simple ISO C interface:
Code:
int DoSomething(int, int, int, complex_result *);
A handler that is given to understand the event `DO_SOMETHING' needs to understand how to provide `complex_result *' as part of the "return" value when that handler has been told that the relevant data is available. That is the duty entirely of such "package" event handlers.
The type `ReplyInterface' doesn't need to know how or even what facilities `complex_result *' provides. It only needs to provide a means to retrieve a `complex_result *' when one is available do to the completion of the related event.
The important part to take from this is related to the duties of a general purpose event handler. It is not your job as the creator of the generic event handler to understand every possible event. Your job is to provide a framework for the creation, management, and communication of events. That is your job.
You can't provide a real implementation of the handler for the `FOO_BAR' event. The client designed the `FOO_BAR' event; the handler which the client created for that event knows how to deal with `complex_result *', and in exactly the same way, a client using a "package" event will be responsible for dealing with a `complex_result *' as the "return" value for the event.
You can, of course, provide also a foundation of events, but these will follow an API in the same way a C++ library will have an API. You can provide a `BLINK_CURSOR' event with all the trimmings. You can do this because you know the API. The API is whatever you'd like it to be for that event.
The change to the example is obvious.
Code:
ReplyInterface myIface; // <-- Maybe shared_ptr.
SomeEventInterface iface;
iface.send(myIface, E);
complex_result * s(GetResult<complex_result>(myIface)); // <-- Maybe shared_ptr.
// handle the results of the event
The client code so marked deals with the results of the event; you have only provided a means to access those results.
What if multiple handlers are all registered for a single event, how will you handle an a priori unknown number of return values, and what if some of them constitute errors?
O_o
The conceptual notion of "returning a value" is entirely unrelated to the C++ notion of "returning a value".
It is entirely possible to use a slot system with types (You may look to the Boost facilities that add contextual information to exceptions using stream operators for one such example.) to do exactly that. By wrapping such a concept into a "value" type (like `std::shared_ptr') one could indeed return multiple unrelated types unknown to the code originating the request for such a "return" value.
The calling code could naturally only inspect the "result" for information it was particularly interested in knowing.
However, I submit that any event is necessarily part of an API including such information as the number and type of "return" values. To follow your logic here, how would multiple event handlers registered to the same event deal with an unknown number of parameters some of which may be optional or invalid? At some point, a decision would need to be made about the nature of the event in question considering how to deal with this scenario or if even dealing with this scenario is appropriate to the given event. Regardless of the solution, it is obviously necessary for all handlers targeting the same event to follow the same API.
Consider: a handler is registered to an event such as `READ_SOME' which obviously reads some data from a communication channel and stores that information for later use. The core information such as a reference to the communication channel and storage area is crucial to the event. It is necessary for the correct function of any handler that such information be available. How this happens is irrelevant; all that is important to the discussion is that this information must be made available. More information, such as the type of communication channel, could be provided through the same mechanism and, by that same mechanic, flagged as "empty" or similar.
*shrug*
Of course, many event systems are designed so that handlers either "nest" or "link" such that the first handler to actually handle an event breaks further processing of that event.
Anybody who disagrees with this?
Every event system I've ever seen has facilities that allow the client to "package" an event into an actual return value.
You can do it however you choose, but helping clients "package" events is the norm.
Soma