Have you always been slightly uncomfortable about the heavy use of opaque macros for the implementation of response tables in OWL? Well, did you know that you can eliminate most of them by simply implementing an override of TEventHandler::Find explicitly? See Windows Message Dispatch in OWLNext for more information on how to do so.
In Owlet, we can now write response tables in pure C++ without any macros. And the resulting code is actually more flexible and friendly to both the user and C++ tools, such as the compiler (for error reporting), debuggers (for stepping through code), smart editors (e.g. Microsoft's IntelliSense) and code formatters. Let's take a closer look.
For example, this is the old implementation of the response table for TButton:
OWL_DEFINE_RESPONSE_TABLE(TButton, TControl)
OWL_EV_WM_GETDLGCODE,
OWL_EV_MESSAGE(BM_SETSTYLE, BmSetStyle),
OWL_END_RESPONSE_TABLE;
In Owlet, all the library macros are prefixed by OWL_, but otherwise this code should be familiar to any OWL user. Building on the overhauled Windows message dispatch machinery (rewritten for 6.40), the implementation of these macros is somewhat simpler than the original OWL 5 implementation. The Owlet macros simply put together a definition of the overidden virtual function GetHandlerFunction (formerly known as Find), declared by the macro OWL_DECLARE_RESPONSE_TABLE in the class definition. Partially expanded, the code would look like this (with conventional formatting):
auto TButton::GetHandlerFunction(const TEvent& owl_Event_) -> TDispatchDelegate
{
using owl_TThis_ = TButton;
return SearchEntries<TButton, TControl>(*this, owl_Event_,
{
OWL_EV_WM_GETDLGCODE,
OWL_EV_MESSAGE(BM_SETSTYLE, BmSetStyle),
});
}
enum {};
Unlike the original OWL implementation, which declares and defines a static member to hold the entries in an array, the Owlet implementation makes use of modern C++ support for initializer lists. This allows us to define and pass the table as part of the call to SearchEntries. The latter function has been rewritten to do all the work of searching any number of bases, passed as template parameters.
The original OWL implementation provides a series of versions of the table definition macro, depending on the number of base classes you wanted to search (DEFINE_RESPONSE_TABLE, DEFINE_RESPONSE_TABLE1, DEFINE_RESPONSE_TABLE2, etc.). Owlet uses a single variadic macro that can take any number of arguments (indicated by ellipsis in the macro parameter list) which are expanded by the built-in symbol __VA_ARGS__ within the macro definition. The base classes passed are forwarded to the variadic function template SearchEntries. Variadic macros and templates were added to C++ in C++11.
Note that although apparently unused, the owl_TThis_ type alias is used within the response table entry macros to generically refer to the enclosing class (in effect, it is an implicit macro parameter). The naming of the event (owl_Event_) and class type (owl_TThis_) is chosen to be cryptic as not to clash with anything within the implementation of any of the table entry macros (e.g. OWL_EV_WM_GETDLGCODE), taking into account that these may be user-defined.
The odd empty enum is part of the response table macro implementation just to force the user to put a semicolon after OWL_END_RESPONSE_TABLE. Now that we have expanded the macro, we can get rid of that wart.
This expanded version of our response table is nice enough to write by hand, rather than use the response table definition macros. However, we want to get rid of all the macros, so let's see what we can do. If we go further and expand the table entry macros as well, the code will look like this:
auto TButton::GetHandlerFunction(const TEvent& owl_Event_) -> TDispatchDelegate
{
using owl_TThis_ = TButton;
return SearchEntries<TButton, TControl>(*this, owl_Event_,
{
{WM_GETDLGCODE, 0, 0, (::owl::CheckSignature<owl_TThis_, WM_GETDLGCODE, ::owl::TDispatch>(&owl_TThis_::EvGetDlgCode),
&::owl::TDispatch<WM_GETDLGCODE>::Decode<owl_TThis_, &owl_TThis_::EvGetDlgCode>)},
{BM_SETSTYLE, 0, 0, (::owl::CheckSignature<owl_TThis_, BM_SETSTYLE, ::owl::TDispatch>(&owl_TThis_::BmSetStyle),
&::owl::TDispatch<BM_SETSTYLE>::Decode<owl_TThis_, &owl_TThis_::BmSetStyle>)},
});
}
Ouch — this isn't readable code. However, much of the complexity stems from the signature check injected into every table entry. However, this is a non-essential feature, just to improve error messages, and we can implement it within helper functions instead, which we'll look at later. So, for now, let's get rid of that cruft.
Also, with everything expanded, we can simplify the naming, and we can get rid of the type alias. We can also remove the namespace qualification. With those simplifications, the resulting code looks much more like an ordinary and readable C++ function:
auto TButton::GetHandlerFunction(const TEvent& event) -> TDispatchDelegate
{
return SearchEntries<TButton, TControl>(*this, event,
{
{WM_GETDLGCODE, 0, 0, &TDispatch<WM_GETDLGCODE>::Decode<TButton, &TButton::EvGetDlgCode>},
{BM_SETSTYLE, 0, 0, &TDispatch<BM_SETSTYLE>::Decode<TButton, &TButton::BmSetStyle>}
});
}
Although the whole thing is now comprehensible, it is still a bit unwieldy to write manually. It would be nice if we could have helper functions, similar to the table entry macros, for use within the table to compose each entry. And why not? It is pretty simple to do this with a function template:
auto TButton::GetHandlerFunction(const TEvent& event) -> TDispatchDelegate
{
return SearchEntries<TButton, TControl>(*this, event,
{
MakeMessageEntry<WM_GETDLGCODE, &TButton::EvGetDlgCode>(),
MakeMessageEntry<BM_SETSTYLE, &TButton::BmSetStyle>()
});
}
The MakeMessageEntry function template is declared with the C++11 keyword constexpr to make sure the table can be generated at compile-time and hence not impart a run-time cost.
Note that the MakeMessageEntry call, unlike the OWL_EV_WM_GETDLGCODE macro instantiation, requires us to pass the address of the member function we want to use. We cannot write a function template that uses a specific handler function, because it may be (and most likely is) protected or private. And even if we had access, we would have to know the class type, which would have required it to be passed in some way.
Another slight inconvenience is that we need to fully qualify a member function to take its address. But that's just how C++ works.
The only macros left now are WM_GETDLGCODE and BM_SETSTYLE. However, these Windows macros just expand to constants, so we'll accept that (although we could easily define proper C++ constants for the Windows messages, e.g. using an enumeration).
The two main benefits we get from writing the response tables as C++ are code comprehension and simpler debugging. By writing C++ code, nothing is hidden inside opaque macros when your read or debug your code. Another benefit is that C++ code is simple to extend. For example, TApplication::GetHandlerFunction does just that. It implements GetHandlerFunction by searching the TDocManager, if present, before searching its own table.
auto TApplication::GetHandlerFunction(const TEvent& event) -> TDispatchDelegate
{
if (DocManager)
if (auto dispatch = DocManager->GetHandlerFunction(event))
return dispatch;
// Search response table.
//
return SearchEntries(*this, event,
{
MakeMessageEntry<WM_SYSCOMMAND, &TApplication::EvSysCommand>(),
MakeCommandEntry<&TApplication::CmExit>(CM_EXIT)
});
}
With C++ code we do not have to do strange hacks to add functionality. As an example, let's take a look at one such hack: "Response Table Hacking". This hack allows an entry to match a series of notification sources, but does so by modifying the implementation of the response table search, and adding new incompatible macros (the new EV_COMMAND_RANGE_AND_ID can only be used in a table defined by the new DEFINE_RESPONSE_TABLE_RANGE). With Owlet and without macros, if we want to handle a set of messages using a single handler, we can now just write explicit code in our GetHandlerFunction implementation that filters out and dispatches those messages before searching the response table. And we don't have to complicate the implementation of the table search.
auto TMyFrame::GetHandlerFunction(const TEvent& event) -> TDispatchDelegate
{
if (event.MsgId == WM_COMMAND &&
event.SourceId >= COMMAND_BASE && event.SourceId <= COMMAND_BASE + 499)
return MakeDispatchDelegate<WM_COMMAND, &TMyFrame::OnCommand, TGeneralCommandDispatch>(this);
else return SearchEntries<TMyFrame, TDecoratedFrame>(*this, event,
{
// ...
});
}
And with response tables written in pure C++, rather than in terms of the old macros, it is much simpler for tools, such as smart editors and code formatters, to make sense of the code.
Feature Requests: #235
News: 2020/05/responsibly-macro-free
Wiki: Knowledge_Base
Wiki: Response_table_hacking
Wiki: Understanding_the_Response_Tables_in_ObjectWindows
Wiki: Windows_Message_Dispatch_in_OWLNext