2

I have a large (~100) number of classes derived from a common base class (Device). Every device can accept a certain subset of a similarly large number of commands. Different commands can have different numbers and types of parameters, so every command is encapsulated by its own type (this could be changed if necessary).

What patterns are there that would allow me to pass a command to a device, given only a pointer/reference to the Device base class, such that the device could access the type and parameters of the command?

Options I've come up with:

  1. The most straightforward method would be to add a separate virtual method accepting each command type to the base class Device. However, this would end up with a large number of virtual methods in the base class that are only overridden in very few of the derived classes.
  2. I considered the visitor pattern for this, but since the number of command classes is approximately equal to the number of device classes, this doesn't really gain anything.
  3. Using RTTI (or an enum/identifier unique to each command) to determine the command type, and then a switch/if to branch to the appropriate code. This feels pretty dirty since it bypasses the normal C++ polymorphism. Also, dynamic_cast is very much frowned upon here, so this option is pretty much out.

Any suggestions for a pattern that would allow this cleanly without a huge number of virtual methods in the Device base class?

zennehoy
  • 6,405
  • 28
  • 55
  • Why do you think option 1 is an issue? – Glenn Teitelbaum Nov 09 '15 at 16:05
  • 1
    @GlennTeitelbaum, that would proliferate/pollute the base class interface with types that are not common to all devices. – R Sahu Nov 09 '15 at 16:07
  • 1
    @GlennTeitelbaum Possibly because I've never had a base class with more than say 20 virtual methods. Also, any new commands would have to be added as new methods to the `Device` base class. Also, see what @RSahu said :) – zennehoy Nov 09 '15 at 16:08
  • In what way are the various subtypes of `Device` related at all? Why are they related if they share almost no functionality? Why are they being passed through the same ... data-pipeline? – Yakk - Adam Nevraumont Nov 09 '15 at 16:09
  • @zennehoy Option 3) can be cleaned up a bit if you used a `std::map`. – PaulMcKenzie Nov 09 '15 at 16:10
  • @Yakk Devices are combined into a system (basically as graph nodes). They all have ports that are handled identically, which can be connected from device to device in identical manner. The commands are used to change parameters of the device, without the system having to know details about the device or command. – zennehoy Nov 09 '15 at 16:12
  • @PaulMcKenzie Interesting - I assume you mean a `std::map` of relevant functors? That would still require a dynamic cast though. – zennehoy Nov 09 '15 at 16:14
  • @zennehoy Ok, so part of the device is uniform. Why are you talking to a `Device` as a `Device` when the interfaces are not related? Suppose one of your Devices is a `ChickenPlucker`. If you want to talk to it as a `ChickenPlucker` later, store a `ChickenPlucker`. Elsewhere you can store pointers to the `Device` part of the `ChickenPlucker`, but you shouldn't be re-casting that to a `ChickenPlucker` later. In short, if you don't know if something is a `ChickenPlucker`, why are you interacting as if it was a `ChickenPlucker`. And if you do know, why not encode that as a type? – Yakk - Adam Nevraumont Nov 09 '15 at 16:15
  • @zennhoy if you keep the map command_ID-to-functor in your base class and let the derived classes populate this map, then you would need only one method handleCommand(id) in the base class. – Archie Nov 09 '15 at 16:16
  • @zennehoy I'm assuming that for option 3), you would give each derived class a numerical value denoting a key. This would also serve as a map key. The data would be the function / functor to use. I don't think you need to cast if you have a `int getkey()` or some such function. – PaulMcKenzie Nov 09 '15 at 16:17
  • (Note there are good answers to the above question that justify it being stored as a `Device`; it is just that there are many, many more bad answers than good.) – Yakk - Adam Nevraumont Nov 09 '15 at 16:17
  • Have you thought about using the Curiously Recurring Template Pattern? – Glenn Teitelbaum Nov 09 '15 at 16:24
  • @Yakk The problem is that lists of commands are loaded from one initialization file, and the system with interconnected devices is loaded from another. The system itself doesn't really care about the type of device or command, it just passes commands to specifically named devices when requested (essentially by the user). If the device doesn't implement the command, an error is issued. Not sure if that counts as a good answer - if not I'd love to hear a suggestion for an alternative. – zennehoy Nov 09 '15 at 16:27
  • @zennehoy Ok, an answer, now with a walkthrough of how to implement it, has been added. Basically, don't erase a type in two spots; the derived devices know what commands they can consume, so write the command deserialization attached to the derived devices, instead of centrally. The rest of the answer is details on how to do this without driving yourself insane. – Yakk - Adam Nevraumont Nov 09 '15 at 20:28

3 Answers3

5

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.

Ilya Popov
  • 3,765
  • 1
  • 17
  • 30
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • While certainly cool, this feels a lot more complicated than what calling one of 100 functions based on an index should need to be... I'll certainly keep the magic switch in mind as a way of converting bounded variables to compile-time constants in the future, but for now will stick to a more explicit switch statement. Thanks for sharing this very nice technique though! – zennehoy Nov 10 '15 at 16:41
  • The complication of doing stuff like this in C++ makes me really appreciate Python, where this takes [6 lines of easily-readable code](https://repl.it/BYpm). – Claudiu Nov 10 '15 at 17:19
  • @Claudiu Sure, but then you are locked into one particular reflection system. The above writes its own method reflection system and adds it to a C++ object; in a sense, it is closer to adding a custom mod to the python compiler, rather than python code. It has some advantages at run time, and the cost (once the setup is done) at the point of use isn't much different (free functions instead of methods, and an enum value instead of a function name). – Yakk - Adam Nevraumont Nov 10 '15 at 18:09
3

A good option may be to use something like boost::variant (you don't have to use boost, specifically, but something like it). It would look something like this.

First you define one type which encapsulates all command types:

struct command1 { ... };
struct command2 { ... };
...
typedef boost::variant<command1, command2, ...> command_t;

Your base device class just has one virtual function, which looks like this:

class Device {
    ...
    virtual return_t run_command(const command_t& cmd) = 0;
    ...
};

Now to process the commands, define a base visitor which does nothing:

struct base_command_visitor : public boost::static_visitor<bool>
{
    bool operator()(const command1& cmd) const { return false; }
    bool operator()(const command2& cmd) const { return false; }
    ...
};

Then your derived classes would look something like this:

class Device1 {
    ...
    virtual return_t run_command(const command_t& cmd) override {
        struct command_visitor : public base_command_visitor {
            using base_command_visitor::operator();

            // only define commands you will use
            bool operator()(const command1& cmd) const { ...; return true; }
            bool operator()(const command5& cmd) const { ...; return true; }
        };

        if (!boost::apply_visitor(command_visitor(), cmd)) {
            // did not implement the command;
        }
    }
    ...
};

Pros:

  • Only one virtual function in the base class.
  • Derived classes only have to write code for the commands they will use, thanks to using base_command_visitor::operator();.

Cons:

  • If you add a new command type, you still have to recompile all the code and the base class still changes (still command_t changes). You pretty much have no way around this unless you use dynamic_cast or equivalents (like void * for the args).
  • Still a fair amount of boilerplate code. But such is C++.
Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • This actually looks very workable. I'll let it settle for a bit - maybe somebody has an idea with a bit less boilerplate. Thanks! – zennehoy Nov 09 '15 at 16:29
1

This is the classic double dispatch problem.

I have ran into this pattern a few times and used the following strategy for dealing with it.

Let's say the base class Command has a function that returns an "id", which could be an integral type, string type, or something else that can be used as a key in a map.

struct Command
{
   typedef  <SomeType> IDType;
   virtual IDType getID() const = 0;
};

The interface of Device could be simplified to:

struct Command;
struct Device
{
   virtual execute(Command const& command) = 0;
};

Let's say DeviceABCD is one of the derived types and that the actual device on which we are operating through a base class pointer/reference is a DeviceABCD. In the first dispatch, the call to execute a command is dispatched to DeviceABCD::execute().

The implementation of DeviceABCD::execute() it is dispatched to another function that does the real work.

You need a framework in place to correctly perform the second dispatch. In the framework:

  1. There needs to be map of "command ID" -> "command executor".
  2. There needs to be way to register a "command executor" given a "command ID".

Based on those, you can get the "command executor" given a "command ID". If there is a "command executor", the command execution can simply be dispatched to the "command executor". If not, you need to deal with the error, most likely by raising an exception.

This framework can be use by all sub-types of Device. Hence, the framework can be implemented in Device itself or in a helper class that is a peer to Device. I prefer the second approach and would recommend creating couple of classes: CommandExecutor and CommandDispatcher.

CommandExecutor.h:

struct CommandExecutor
{
   virtual execute(Command const& command) = 0;
};

CommandDispatcher.h:

class CommandDispatcher
{
   public:
      void registerCommandExecutor(Command::IDType commandID,
                                   CommandExecutor* executor);

      void executeCommand(Command const& command);

      std::map<Command::IDType, CommandExecutor*>& getCommandExecutorMap();

   public:
      std::map<Command::IDType, CommandExecutor*> theMap;
};

CommandDispatcher.cpp:

void CommandDispatcher::registerCommandExecutor(Command::IDType commandID,
                                                CommandExecutor* executor)
{
   getCommandExecutorMap()[commandID] = executor;
}

void CommandDispatcher::executeCommand(Command const& command)
{
   CommandExecutor* executor = getCommandExecutorMap()[commandID];
   if ( executor != nullptr )
   {
      executor->execute(command);
   }
   else
   {
      throw <AnAppropriateExecption>;
   }
}

std::map<Command::IDType, CommandExecutor*>& CommandDispatcher::getCommandExecutorMap()
{
   return theMap;
}

If DeviceABCD can execute Command12 and Command34, its implementation would look something like:

DeviceABCD.cpp:

struct Command12Executor : public CommandExecutor
{
    virtual void execute(Command const& command) { ... }
};

struct Command34Executor : public CommandExecutor
{
    virtual void execute(Command const& command) { ... }
};

DeviceABCD::DeviceABCD() : commandDispatcher_(CommandExecutor)
{
   static Command12Executor executor12;
   static Command34Executor executor34;

   // This assumes that you can get an ID for all instances of Command12
   // without an instance of the class, i.e. it is static data of the class.

   commandDispatcher_.registerExecutor(Command12Type, &executor12);
   commandDispatcher_.registerExecutor(Command34Type, &executor34);
}

With that framework in place, the implementation of DeviceABCD::execute() is very simple.

void DeviceABCD::execute(Command const& command)
{
   commandDispatcher_.executeCommand(command);
}

This is streamlined to a point where it can even be implemented in the base class. You need to implement it in the derived class only if there is a need to massage the command or update some other state before the command gets dispatched to the right CommandExecutor.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • Good call on identifying this as double dispatch. This has a big issue, though. What if you have two instances of `DeviceABCD`, or two different devices that can both handle a command of a given type? Because the dispatch map is static, they will overwrite each other and issuing the command on the first instance will end up calling the function of the second instance. – Claudiu Nov 09 '15 at 16:51
  • @Claudiu, in that case, you'll need to separate `CommandExecutor` into two classes - `CommandExecutor` and `CommandDispatcher` and let each instance of `DeviceABCD` have their own `CommandDispatcher`. – R Sahu Nov 09 '15 at 16:59
  • Double dispatch - I knew this problem looked familiar somehow, I just couldn't come up with the appropriate name. This or something very like it is the way I'm going right now, so I'll accept this as the answer. – zennehoy Nov 10 '15 at 16:47