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.
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.