This kind of code is fairly normal. You have different classes, and at some point it is necessary to decide which class shall be used.
However, the limitation is that you have hard-coded support for a fixed number of classes. Depending on the context of your application, it could make more sense to create a data structure where classes can be dynamically registered, e.g. via plugins. That would allow new classes to be added without recompiling your entire application. Something like this:
using Constructor = std::function<std::unique_ptr<DataSource>(Config, Display)>;
std::unordered_map<std::string, Constructor> registry& registry() {
// keep the static variable inside a function to control initialization order
static std::unordered_map<std::string, Constructor> r;
return r;
}
// selecting the appropriate implementation
std::unique_ptr<DataSource> create_data(std:string_view ds, Config c, Display d) {
return registry().at(ds)(c, d);
}
int main() {
// initialization: register available implementations
registry().emplace("mqtt", [](Config config, Display display) {
return std::make_unique<Data_mqtt>(config, display);
});
registry().emplace("wf", [](Config config, Display display) {
return std::make_unique<Data_wf>(config, display);
});
// ... some application code
return 0;
}
This is the manually implemented version of what's performed in other languages by a dependency injection container. In some languages, reflection capabilities could also be used. Additionally, more complex DI containers can also track dependencies between the objects they construct, e.g. they might be able to automatically resolve the Config or Display objects. In C++, that would only be possible with lots of casting.
Whether such a registry makes sense for your software depends on whether you want to be able to potentially keep the software extensible without recompilation. This is extremely useful for libraries or for plugin architectures, but is mostly useless for more monolithic applications.
Your point that the use of a DataSource
interface limits the available functions is very good. This is an example of the interface segregation principle where you try to avoid offering a larger interface than what's actually needed, in order to prevent unexpected dependencies.