Search code examples
c++c++17run-time-polymorphism

Runtime type selection based on map of types


Background

I am writing an application which utilizes USB devices. This includes device discovery of the USB devices my application can use, based on USB vendor id and product id. However these devices sometimes have multiple possibly working drivers i.e. application implementations, depending on platform and moonphase (costumer legacy stuff). So I want to use runtime polymorphism using std::shared_ptr with a nice family of interfaces and stuff.

Problem

I can not figure out how to make_shared an object of a certain type, based on a runtime given key. At least not in an non ugly sense.

Solution so far

I was thinking about storing a type value somehow into a map known_drivers (in this example a multimap but not a big difference) so that in the end, depending on the number value I can construct a different class type and stuff it into a shared_ptr.

Example code

#include <iostream>
#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <stdexcept>

using vendor_product_usb_id_t = std::pair<uint16_t, uint16_t>;

struct usb_device {
    static std::shared_ptr<usb_device> open(vendor_product_usb_id_t);
    virtual void do_stuff() = 0;
};

struct usb_device_using_driver_a : usb_device {
    usb_device_using_driver_a() {throw std::runtime_error("Not supported on this platform");}
protected:
    void do_stuff() override {}
};

struct usb_device_using_driver_b : usb_device {
protected:
    void do_stuff() override {std::cout << "Stuff B\n";}
};


const std::multimap<vendor_product_usb_id_t, ??> known_drivers = {{{0x42,0x1337}, driver_a}, {{0x42,0x1337}, driver_b}};

std::shared_ptr<usb_device> usb_device::open(vendor_product_usb_id_t id) {
    std::shared_ptr<usb_device> value;
    for (auto [begin,end] = known_drivers.equal_range(id); begin != end; ++begin) {
        try {
            value = std::make_shared<*begin>();
        } catch (std::exception& e) {
            continue;
        }
    }
    return value;
}

int main() {
    auto device = usb_device::open(std::make_pair(0x42,0x1337));
    if (device) {
        device->do_stuff();
    }
}

Solution

  • Storing the type and using it to create an instance is not possible in C++, since C++ does not have reflection (yet).

    However, what you want is possible with the factory pattern and the virtual constructor idiom.

    As a basic starting point:

    using vendor_product_usb_id_t = std::pair<uint16_t, uint16_t>;
    
    struct usb_device {
        using usb_ptr = std::shared_ptr<usb_device>;
    
        virtual void do_stuff() = 0;
    
        // virtual constructor
        virtual usb_ptr create(vendor_product_usb_id_t) = 0;
    };
    
    
    struct usb_device_using_driver_a : usb_device {
        usb_ptr create(vendor_product_usb_id_t) override {
            return usb_ptr(new usb_device_using_driver_a);
        }
    
        void do_stuff() override {
            std::cout << "usb_device_using_driver_a::do_stuff()" << std::endl;
        }
    };
    
    
    struct usb_device_using_driver_b : usb_device {
        usb_ptr create(vendor_product_usb_id_t) override {
            throw std::runtime_error("Not supported on this platform");
        }
    
        void do_stuff() override {
            std::cout << "usb_device_using_driver_b::do_stuff()\n";
        }
    };
    
    
    class usb_device_factory {
    public:
    
        static usb_device::usb_ptr open(vendor_product_usb_id_t id) {
            // note this map is static
            // for simplicity changed the multimap to map
            static std::map<vendor_product_usb_id_t, usb_device::usb_ptr> known_drivers = {
                std::make_pair(std::make_pair(0x42,0x1337), usb_device::usb_ptr(new usb_device_using_driver_a())),
                std::make_pair(std::make_pair(0x43,0x1337), usb_device::usb_ptr(new usb_device_using_driver_b())),
            };
    
            return known_drivers[id]->create(id);
        }
    };
    
    
    int main() {
        try {
            auto device = usb_device_factory::open(std::make_pair(0x42,0x1337));
            device->do_stuff();
    
            // this will throw
            device = usb_device_factory::open(std::make_pair(0x43,0x1337));
            device->do_stuff();
        }
        catch(std::exception const& ex) {
            std::cerr << ex.what();
        }
    }
    

    Live.

    Also possible without the virtual constructor idiom, but the basic principle of factory pattern still applies:

    using vendor_product_usb_id_t = std::pair<uint16_t, uint16_t>;
    
    
    struct usb_device {
        virtual void do_stuff() = 0;
    };
    
    using usb_ptr = std::shared_ptr<usb_device>;
    
    
    
    struct usb_device_using_driver_a : usb_device {
        void do_stuff() override {
            std::cout << "usb_device_using_driver_a::do_stuff()" << std::endl;
        }
    };
    
    
    struct usb_device_using_driver_b : usb_device {
        void do_stuff() override {
            std::cout << "usb_device_using_driver_b::do_stuff()\n";
        }
    };
    
    
    class usb_device_factory {
    public:
    
        static usb_ptr open(vendor_product_usb_id_t id) {
            // note this map is static
            static std::map<vendor_product_usb_id_t, std::function<usb_ptr()>> known_drivers = {
                std::make_pair(std::make_pair(0x42,0x1337), []() { return usb_ptr(new usb_device_using_driver_a()); } ),
                std::make_pair(std::make_pair(0x43,0x1337), []() { return usb_ptr(new usb_device_using_driver_b()); } ),
            };
    
            return known_drivers[id]();
        }
    };
    
    
    int main() {
        try {
            auto device = usb_device_factory::open(std::make_pair(0x42,0x1337));
            device->do_stuff();
    
            device = usb_device_factory::open(std::make_pair(0x43,0x1337));
            device->do_stuff();
        }
        catch(std::exception const& ex) {
            std::cerr << ex.what();
        }
    }
    

    Live.

    With both variants it is possible to extend the map at runtime with additional driver. Since it is stored in a "dynamic" map, there is no switch limiting the amount of drivers.

    Edit:

    Just found a similar question, where the answer is basically the same as mine, unsing factory pattern with virtual constructors:

    Can objects be created based on type_info?