The most straightforward way to deal with this would be to use the adapter pattern where the adapters all inherit from an "interface" type (pure virtual methods only). To do this, you need to know what adapted operations you need to perform on each list.
For example, given the single operation "add an item to the list" we can use std::string
as the argument type, since this will likely be the type of input anyway. So, our base adapter interface looks like:
class le_adapter {
public:
virtual ~le_adapter() = default;
virtual bool add(std::string const &) = 0;
};
Now we need an implementation of the adapter for each list type. Thankfully, templates can help us here:
template <typename T>
class le_adapter_impl : public le_adapter {
public:
virtual bool add(std::string const &) override;
le<T> & get();
private:
le<T> list;
};
template <typename T>
bool le_adapter_impl<T>::add(std::string const & str) {
// Naive implementation; should do more error checking.
T value;
std::istringstream source{str};
if (source >> value) {
list.add(value);
return true;
}
return false;
}
template <typename T>
le<T> & le_adapter_impl<T>::get() {
return list;
}
Now we have an adapter we can put in front of a list of a given type and use it polymorphically through le_adapter
without needing to know what kind of list is held within. (Note that a "proper" adapter would store a reference/pointer instead of an object, but having the adapter own the list simplifies this example.)
Now, you can just construct a different adapter depending on the user's input. For example, you could store factory functions in a map:
std::map<std::string, std::function<std::unique_ptr<le_adapter>()>> factories{
{"int", std::make_unique<le_adapter_impl<int>>},
{"char", std::make_unique<le_adapter_impl<char>>},
{"float", std::make_unique<le_adapter_impl<float>>},
{"double", std::make_unique<le_adapter_impl<double>>},
};
Now you have a map you can use to dispense adapters depending on what the user enters:
std::string input{"int"}; // Example user input
auto i = factories.find(input);
if (i != factories.end()) {
auto adapter = i->second(); // Creates the desired adapter.
adapter->add("123"); // This would also be user input
} else {
// Handle invalid selection
}
Now we have adapter
which is std::unique_ptr<le_adapter>
. You don't have to know which specific implementation is used in order to use the virtual methods defined on the base interface.
The adapter is only as useful as its base interface; if you need to support other operations then you have to define them on that interface, and they have to have generic arguments or return values to be useful.
If you need to perform extensive manipulation/calculation that would benefit from knowing the type of T
then you could use a template function and apply it to the adapter using the visitor pattern through an overloaded functor. (Or cheat and do that in the adapter, though that could be considered to violate the separation of concerns.)