Search code examples
c++dependency-injectiontype-safety

Typesafe way to provide enriched dependencies to derived classes in C++


My title is probably not great -- I'm open to suggestions.

Right now I have library code that provides base functionality for a class that has some dependencies. The code looks something like this:

class Fan { /* ... */ };
class PanametricFan : public Fan { /* ... */ };
class DeskFan : public Fan { /* ... */ };

class LogCasing { /* ... */ };
class MalleableLogCasing : public LogCasing { /* ... */ };

struct EncabulatorDependencies
{
  Fan * fan;
  LogCasing * casing;
};

EncabulatorDependencies setupDependencies
  (const std::string & confFile) { /* ... */ }

class Encabulator
{
  public:
    Encabulator(const EncabulatorDependencies & dependencies);

  // ...
};

Now in user code, I have something like this:

class TurboEncabulator : public Encabulator { /* ... */ }

Now while the base class functionality in Encabulator can do just fine with any sort of Fan and LogCasing, the TurboEncabulator specifically needs a Panametric Fan and a MalleableLogCasing to work.

Of course if the config file was set up properly, then setupDependencies will give me the right kind of fan and casing, which I could just cast them, like so:

TurboEncabulator::TurboEncabulator(const EncabulatorDependencies & deps)
: Encabulator(deps)
{
  myPanametricFan = dynamic_cast<PanametricFan *>(deps.fan);
  myMalleableCasing = dynamic_cast<MalleableLogCasing *>(deps.casing);

  //..
}

But I'd like to do something a little more typesafe.

My idea was to replace EncabulatorDependencies with a list of variants and have something that says, say, "give me a Fan * from this collection" (including, say, a pointer to any derived class of Fan), or "give me a PanametricFan * from this collection." Something like

Encabulator::Encabulator(const EncabulatorDependencies & deps)
{
  myFan = deps.get<Fan *>();
  myCasing = deps.get<LogCasing *>();
}

in the library and

TurboEncabulator::TurboEncabulator(const EncabulatorDependencies & deps)
{
  myPanametricFan = deps.get<PanametricFan *>();
  myMalleableCasing = deps.get<MalleableLogCasing *>();
}

in the user code.

Then the setupDependencies method can just build the appropriate derived-type object and throw a pointer into the dependency list, the Encabulator can have its Fan *, the TurboEncabulator can have is PanametricFan *, and if the config file isn't set up properly the system can produce a sane error message.

Of course I can think of a couple of "dirty" ways this could be accomplished -- EncabulatorDependencies could just hold separate Fan *, Panametric Fan *, Desk Fan *, etc. pointers and client code could grab the one it wants, for instance -- but I was hoping there was a "right" way to do this that doesn't incur extra maintenance costs.


Solution

  • I was able to solve this problem as follows:

    First, I need a base Tool class that all the classes inherit from, and this class needs to have runtime type information (RTTI). The easiest way to do this is to give it a virtual method, since this essentially forces it to have a vtable. And since anything that has virtual methods needs a virtual destructor anyway, it's okay to make this class nothing but a virtual destructor:

    class Tool
    {
      public:
    
        virtual ~Tool() = default;
    };
    

    Next, I just need something that holds all my tools:

    std::vector<Tool *> myTools;
    

    Now when I want a tool, all I have to do is iterate through the vector and attempt to convert things to my type until I find one that will convert successfully:

    template<class T> T& grabTool()
    {
      static_assert<std::is_base_of_v<Tool, T>);
    
      for(Tool * t : myTools)
      {
        try
        {
          return dynamic_cast<T&>(*t);
        }
        catch(const std::bad_cast &) { }
      }
    
      throw std::out_of_range(typeid(T).name());
    }
    

    Note that we need to work with references instead of working directly with pointers here, since dynamic_cast will happily a base pointer to a derived pointer without checking what it's actually pointing at.