Tutorial 11
Avoiding virtual functions.
All the previous tutorials exposed polymorphic behavior when defining common message interface class. The COMMS Library also properly supports message classes without any virtual functions.
Let's take a look at inner definitions inside src/ServerSession.h.
class ServerSession : public Session { public: // Common interface class for all the messages using Message = tutorial11::Message<>; };
The common message interface class does not receive any extension options, hence does NOT
expose any polymorphic behavior, i.e. there are no virtual functions. As the result all the
actual messages (Msg1, Msg2, ...) that extend
comms::MessageBase don't
have any v-tables and are equivalent to being simple structs of data.
It was explained in one of the previous tutorials that
message dispatch logic can
also generate polymorphic dispatch table with virtual functions. If the main goal is to
avoid virtual functions altogether, not just for messages, then the dispatch when processing
incoming message needs to be forced to be "static-binary-search" one or the
switch statement based provided by the generated code.
std::size_t ServerSession::processInputImpl(const std::uint8_t* buf, std::size_t bufLen) { ... // Force switch statement based dispatch using Dispatcher = tutorial11::dispatch::ServerInputMsgDispatcher<ServerProtocolOptions>; // Process reported input, create relevant message objects and // dispatch all the created messages // to this object for handling (handle() member function will be called) return comms::processAllWithDispatchViaDispatcher<Dispatcher>(buf, bufLen, m_frame, *this); }
There is another place that can potentially contain virtual functions. It's when the message framing is processed, there is a need to map numeric message ID into appropriate message class. The COMMS Library uses the same dispatch logic for this process. If the message IDs are sequential with less than 10% holes, then the polymorphic dispatch tables may be created. To avoid that there is a need to request "static-binary-search" dispatch for this process as well. In order to understand how to do it there is a need to dive a little bit into the COMMS Library internals.
The framing uses comms::frame::MsgIdLayer
which is responsible to read received numeric message ID and create appropriate message object.
In order to create such object the
comms::frame::MsgIdLayer
contains comms::MsgFactory in its
private data members. The
contained comms::MsgFactory
can be configured to use appropriate dispatch logic via its options. The relevant options are:
comms::option::app::ForceDispatchPolymorphic, comms::option::app::ForceDispatchStaticBinSearch, or
comms::option::app::ForceDispatchLinearSwitch. The
comms::frame::MsgIdLayer
has its own supported options which it processes. The ones it doesn't expect are passed to the
comms::MsgFactory.
It means in order to avoid creation of virtual functions inside
comms::MsgFactory the
comms::frame::MsgIdLayer
needs to receive comms::option::app::ForceDispatchStaticBinSearch option.
The used Frame inside the include/tutorial11/frame/Frame.h is defined
like this:
template <typename TOpt = tutorial11::options::DefaultOptions> struct FrameLayers { ... /// @brief Definition of layer "Id". template <typename TMessage, typename TAllMessages> using Id = comms::frame::MsgIdLayer< ..., typename TOpt::frame::FrameLayers::Id >; ... template<typename TMessage, typename TAllMessages> using Stack = ... }; template < typename TMessage, typename TAllMessages = tutorial11::input::AllMessages<TMessage>, typename TOpt = tutorial11::options::DefaultOptions > class Frame : public FrameLayers<TOpt>::template Stack<TMessage, TAllMessages> { ... };
NOTE that there is a way to pass extra options to the definition of Id layer.
To do so this tutorial defines separate protocol options (src/ProtocolOptions.h)
shared by both server and client.
template <typename TBase = tutorial11::options::DefaultOptions> struct ProtocolOptionsT : public TBase { struct frame : public TBase::frame { struct FrameLayers : public TBase::frame::FrameLayers { using Id = std::tuple< comms::option::app::ForceDispatchStaticBinSearch, typename TBase::frame::FrameLayers::Id >; }; // struct FrameLayers }; // struct frame }; using ProtocolOptions = ProtocolOptionsT<>;
Please pay attention that the code above is written to support extension of any pre-defined options that can be passed as a template parameter.
The definition of the frame inside src/ServerSession.h is:
class ServerSession : public Session { public: ... // Common interface class for all the messages using Message = tutorial11::Message<>; // Protocol options for server using ServerProtocolOptions = ProtocolOptionsT< tutorial11::options::ServerDefaultOptions >; ... private: using Frame = tutorial11::frame::Frame< Message, tutorial11::input::ServerInputMessages<Message, ServerProtocolOptions>, ServerProtocolOptions >; ... };
Note that the schema file of this tutorial does not specify uni-directional messages, i.e. the definitions of the tutorial11::input::AllMessages, tutorial11::input::ServerInputMessages, and tutorial11::input::ClientInputMessages look exactly the same. Also the protocol options defined as tutorial11::options::DefaultOptions, tutorial11::options::ServerDefaultOptions, and tutorial11::options::ClientDefaultOptions do not differ in terms of defined options. It is still a good practice to force server and/or client application specific configuration even if it doesn't differ from the general case (usage of tutorial11::input::AllMessages and/or tutorial11::options::DefaultOptions). There is always a potential for uni-directional message to be added to the protocol in the future which will cause the configurations to be different.
Another important thing to understand is how the outgoing message is serialized. Due to the fact that the message interface class does NOT expose any polymorphic behavior, the knowledge about real message type is needed to be able to write message numeric ID as well as properly write all message fields as the payload. There is no other choice but to make the serialization function to be a template one:
class ServerSession : public Session { /// private: template <typename TMsg> void sendMessage(TMsg& msg) { ... } };
Another curious thing to notice is a lack of virtual destructor for the common message interface
class (due to the lack of any polymorphic behaviour). During the read() operation the frame
dynamically allocates proper message object and holds it by the std::unique_ptr to the
common message interface class (see Frame::MsgPtr).
After reading these statements any experienced C++ developer should scream about
incorrect message destruction and potential memory leaks. HOWEVER, this is not the case
with the COMMS Library. It
recognizes lack of virtual destructor at compile-time and uses a custom deleter for
the std::unique_ptr. The deleter stores numeric ID of the allocated message object and uses
Static Binary Search
way of dispatching to map stored numeric ID into appropriate message type. Before actual
deletion the provided pointer is cast to the right class and properly destructed.
To test this, all of the available message definitions inside include/tutorial11/message were modified to print their destructor function name when it is invoked:
template <typename TMsgBase, typename TOpt = tutorial11::options::DefaultOptions> class Msg1 : public comms::MessageBase<...> { public: /// @brief Custom destructor ~Msg1() { std::cout << "Proper destruction: " << __FUNCTION__ << std::endl; } ... };
The client side of this tutorial sends Msg1, Msg2, and Msg3 to the
server. The latter identifies the messages, dynamically allocates appropriate message object
(held by std::unique_ptr to the common interface class), dispatches it to the handling function,
and then destructs the message object when it's no longer needed. The output produced by the server application
looks like this:
Processing input: 0 1 1
Received message "Message 1" with ID=1
Proper destruction: ~Msg1
Processing input: 0 1 2
Received message "Message 2" with ID=2
Proper destruction: ~Msg2
Processing input: 0 1 3
Received message "Message 3" with ID=3
Proper destruction: ~Msg3
SIDE NOTE: The commsdsl2comms code generator allows injection
of custom C++ code snippets into the generated one. It is explained in
Injecting Custom Code
section of commsdsl2comms manual documentation page.
The dsl_src folder of this tutorial contains code snippets injected into the message definitions.
The dsl_src/include/tutorial11/message/MsgX.h.inc contains
extra include statements to be added at the beginning of MsgX.h file, while
dsl_src/include/tutorial11/message/MsgX.h.public
contains extra code to be added to the public section of the message definition.
The ClientSession is defined in very similar way but choosing client specific configuration:
class ClientSession : public Session { public: // Common interface class for all the messages using Message = tutorial11::Message<>; // Protocol options for client using ClientProtocolOptions = ProtocolOptionsT< tutorial11::options::ClientDefaultOptions >; // Definition of all the used message classes using Msg1 = tutorial11::message::Msg1<Message, ClientProtocolOptions>; using Msg2 = tutorial11::message::Msg2<Message, ClientProtocolOptions>; using Msg3 = tutorial11::message::Msg3<Message, ClientProtocolOptions>; private: // Send the message requires knowledge about the full message type template <typename TMsg> void sendMessage(const TMsg& msg) { ... } // Client specific frame using Frame = tutorial11::frame::Frame< Message, tutorial11::input::ClientInputMessages<Message, ClientProtocolOptions>, ClientProtocolOptions >; };
The dispatch of the client input messages was also chosen to be the
switch statement based.
std::size_t ClientSession::processInputImpl(const std::uint8_t* buf, std::size_t bufLen) { ... // Force switch statement based dispatch using Dispatcher = tutorial11::dispatch::ClientInputMsgDispatcher<ClientProtocolOptions>; // Process reported input, create relevant message objects and // dispatch all the created messages // to this object for handling (appropriate handle() member function will be called) return comms::processAllWithDispatchViaDispatcher<Dispatcher>(buf, bufLen, m_frame, *this); }
Summary
- The COMMS Library provides multiple means to avoid any polymorphic functionality, i.e. virtual functions.
- When no polymorphic behavior options are passed to the common interface definition, the message objects don't have any v-table and their destructors are non-virtual.
- All the dynamically allocated message objects are still held by the
std::unique_ptrto the common message interface class, but with a custom deleter which insures correct destruction and de-allocation of the object. - The framing layer responsible for allocation of the message object is comms::frame::MsgIdLayer, which uses comms::MsgFactory in its private data members to allocate appropriate message object when the ID value is known.
- The message allocation options passed to the comms::frame::MsgIdLayer are also forwarded to comms::MsgFactory. They can be used to force a particular dispatch policy for mapping numeric message ID into the message type.
- In order to forcefully avoid generation on polymorphic dispatch table inside
comms::MsgFactory the
comms::option::app::ForceDispatchStaticBinSearchoption needs to be passed to theIdlayer definition of the framing. - The generated dispatch functions and classes inside include/<namespace>/dispatch folder can also be used for dispatch functionality when virtual functions are been avoided.
Read Previous Tutorial <-----------------------> Read Next Tutorial