Your system consists of a network of Devices, and a list of Commands that are dispatched to each Device.
You have code that deserializes the Commands and dispatches them to the Devices. I think this is the wrong order.
Instead, the Command should be sent serialized (or, in a "common form" or "unparsed form" -- a vector of strings for arguments, and an int for command id) to the Device. The Device uses common (template probably) code within "itself" to deserialize the Command and invoke it on itself.
At the point of invocation, the Device knows its own type. The template deserialization code can be told what kinds of commands the Device understands, and given an invalid command can error out and fail statically. Given a valid command, it can invoke it on the Device in a type-safe manner.
The commands can be partially deserialized at the point they are passed to the Device if you want.
If you add a new command or device type, this does not require recompiling any existing Device. The old devices parser should be robust enough to detect and discard the invalid command. The new devices parser will consume the new command type.
The "execute serialized command" interface has to have a return value that indicates if the command was an invalid one, so you can handle it outside of the Device interface. This could involve error code, std::experimental::expected
-type patterns, or exceptions.
Here is a sketch of an implementation.
Writing the "execute command" code efficiently (in terms of no DRY and runtime efficiency) from serialized data is a bit tricky.
Suppose we have an enum of commands, called "command_type
". The number of known command type is command_type::number_known
-- all command types need to have values strictly less than that.
Next, add a function that looks like this:
template<command_type t>
using command_t = std::integral_constant<command_type, t>;
template<command_type t, class T>
error_code execute_command( command_t<t>, T const&, std::vector<Arg>const&){
return error_code::command_device_mismatch;
}
This is the default behavior. By default, a command type and a device type do not work together. Arguments are ignored, and an error is returned.
We also write a helper type, for use later. It is a template<size_t>class
that calls execute_command
in an ADL (argument dependent lookup) friendly way. This class template should be in the same namespace as execute_command
.
template<size_t N>
struct execute_command_t {
template<class T>
error_code operator()( T const& t, std::vector<Arg>const& a){
return execute_command(command_t<static_cast<command_type>(N)>{}, t, a);
}
};
They should both be pretty universally visible.
We then proceed to create execute_command
overloads that are only privately visible to the implementations of various Device subtypes.
Suppose we have a type Bob
, and we want Bob
to understand the command command_type::jump
.
We define a function in Bob
's file that looks like this:
error_code execute_command( command_t<command_type::jump>, Bob& bob, std::vector<Arg> const& args );
This should be in the same namespace as the Bob
type.
We then write a magic switch. A magic switch takes a runtime value (enum value in this case), and maps to a table of functions that instantiate an array of compile-time templates with that runtime value. Here is an implementation sketch (it is not compiled, I just wrote it off the top of my head, so it can contain errors):
namespace {
template<template<size_t>class Target, size_t I, class...Args>
std::result_of_t<Target<0>(Args...)> helper( Args&&... args ) {
using T=Target<I>;
return T{}(std::forward<Args>(args)...);
}
}
template<size_t N, template<size_t>class Target>
struct magic_switch {
private:
template<class...Args>
using R=std::result_of_t<Target<0>(Args...)>;
template<size_t...Is, class...Args>
R<Args...> execute(std::index_sequence<Is...>, size_t I, R<Args...> err, Args&&...args)const {
using Res = R<Args...>;
if (I >=N) return err;
using f = Res(Args&&...);
using pf = f*;
static const pf table[] = {
// [](Args&&...args)->Res{
// return Target<Is>{}(std::forward<Args>(args)...);
// }...,
&helper<Target, Is, Args...>...,
0
};
return table[I](std::forward<Args>(args)...);
}
public:
template<class...Args>
R<Args...> operator()(size_t I, R<Args...> err, Args&&...args)const {
return execute( std::make_index_sequence<N>{}, I, std::forward<R<Args...>>(err), std::forward<Args>(args)... );
}
};
magic_switch
itself is a template. It takes a max value it can handle N
and a template<size_t>class Target
that it will create and call.
(The commented out code with the lambda is legal C++11, but neither gcc5.2 nor clang 3.7 can compile it, so use the helper
version.)
Its operator()
takes an index (size_t I
), a error for out-of bounds err
, and a pack of arguments to perfect forward.
operator()
creates a index_sequence<0, 1, ..., N-1>
and passes it to the private execute
method. execute
uses that index_sequence
's integers to create an array of function pointers, each one of which instantiates Target<I>
and passes it Args&&...args
.
We bounds check and then do an array lookup into that list with our runtime argument I
, then run that function pointer, which calls Target<I>{}(args...)
.
The above code is generic, not specific to this problem. We now need some glue to make it work with this problem.
This function takes the magic_switch
above, and merges it with our ADL dispatched execute_command
:
template<class T>
error_code execute_magic( command_type c, T&& t, std::vector<Arg> const& args) {
using magic = magic_switch< static_cast<size_t>(command_type::number_known), execute_command_t >;
return magic{}( size_t(c), error_code::command_device_mismatch, std::forward<T>(t), args );
}
Our execute_command_t
template is passed to magic_switch
.
In the end, we have a run-time "jump table" of function pointers pointing to code that does a execute_command( command_t<command_type::???>, bob, args )
for each command_type
enum value. We take a runtime command_type
and do an array lookup with it, and call the appropriate execute_command
.
If no execute_command( command_t<command_type::X>, bob, args )
overload has been specifically written, the default one (way up at the start of this sample) is called, and it returns a command-device mismatch error. If one has been written, it is found via the magic of Argument Dependent Lookup, and it is more specialized than the generic overload that fails, so it is called instead.
If we are fed a command_type
that is out of range, we also handle that. So no need to recompile each Device when a new command_type
is created (it is optional): they will still work.
This is all fun and dandy, but how do we get execute_magic
to be called with the real device sub-type?
We add a pure virtual method to Device
:
virtual error_code RunCommand(command_type, std::vector<Arg> const& args) = 0;
We could custom-implement RunCommand
in each derived type of Device
, but that violates DRY and can lead to bugs.
Instead, write a CRTP (curiously recurring template pattern) indermediarey helper called DeviceImpl
:
template<class Derived>
struct DeviceImpl {
virtual error_code RunCommand(command_type t, std::vector<Arg>const& args) final override
{
auto* self = static_cast<Derived*>(this);
return execute_magic( t, *self, args );
}
};
Now, when we define the command Bob
we do:
class Bob : public DeviceImpl<Bob>
instead of inheriting from Device
directly. This auto-implements Derived::RunCommand
for us, and avoids a DRY issue.
The declaration of execute_command
that takes a Bob&
overload must be visible prior to DeviceImpl<Bob>
being instantiated, or the above doesn't work.
The final bit is implementing execute_command
. Here we have to take the std::vector<Arg> const&
and invoke it on Bob
properly. There are many stack overflow questions on this problem.
In the above, a handful of C++14 features are used. They can be easily written in C++11.
Key techniques used:
Magic Switch (what I call the technique of a run-time jump table to compile-time template instances)
Argument Dependent Lookup (how execute_command
is found)
Type Erasure or Run-Time Concepts (We are type-erasing the execution of the command into the virtual RunCommand
interface)
Tag Dispatching (we pass command_t<?>
as a tag type to dispatch to the correct overload of execute_command
).
CRTP (Curiously Recurring Template Pattern), which we use in DeviceImpl<Derived>
to implement the RunCommand
virtual method once, in a context where we know the Derived
type, so we can then dispatch properly.
live example.