polymorphismc++20visitor-patterncommand-pattern

Why can't std::visit disambiguate templated overloads


I'm trying to build a command pattern where each command has access to a defined interface. Receivers implement one or more of these interfaces and can then have commands, by way of CommandLists applied to them. I've included the code below and a Compiler Explorer link. I was hoping that both templates would apply, create an overload set, and then std::visit (with the help of a lambda) would be able to disambiguate on the basis of the arguments, but that's obviously not how it works. Can someone please explain why this isn't working, and what options I have to resolve? Thanks!

<source>: In instantiation of 'void MultiReceiver<Interface, Interfaces>::applyCommandList(CommandList) [with Interface = ICatalogue; Interfaces = {IInventory}; CommandList = std::vector<std::variant<std::shared_ptr<Command<ICatalogue> >, std::shared_ptr<Command<IInventory> > >, std::allocator<std::variant<std::shared_ptr<Command<ICatalogue> >, std::shared_ptr<Command<IInventory> > > > >]':
<source>:110:26:   required from here
<source>:41:53: error: request for member 'applyCommand' is ambiguous
   41 |                 std::visit([=,this](auto& c){ this->applyCommand(*c); }, command);
      |                                               ~~~~~~^~~~~~~~~~~~
<source>:30:14: note: candidates are: 'void Receiver<Interface>::applyCommand(Command<Receiver<Interface> >) [with Interface = IInventory]'
   30 |         void applyCommand(Command<Receiver> command) {
      |              ^~~~~~~~~~~~
<source>:30:14: note:                 'void Receiver<Interface>::applyCommand(Command<Receiver<Interface> >) [with Interface = ICatalogue]'
Compiler returned: 1

godbolted

#include <cinttypes>
#include <functional>
#include <memory>
#include <iostream>
#include <vector>
#include <variant>
#include <unordered_map>

template <class Receiver>
class Command {
    public:
        virtual void execute(Receiver& receiver) = 0;
};

class Product {
    public:
        Product(std::string name): _name(name) {};
        std::string getName() const { return _name; };
        bool operator==(const Product&) const = default;
    private:
        std::string _name;
};

template<class ...Receivers>
using CommandList = std::vector<std::variant<std::shared_ptr<Command<Receivers>>...>>;

template <class Interface>
class Receiver {
    public:
        void applyCommand(Command<Receiver> command) {
            command.execute(*this);
        }
};

template <class Interface, class ...Interfaces>
class MultiReceiver: public Receiver<Interface>, public Receiver<Interfaces>... {
    public:
        using CommandList = ::CommandList<Interface, Interfaces...>;
        void applyCommandList(CommandList commands) {
            for(const auto& command: commands ) {
                std::visit([=,this](auto& c){ this->applyCommand(*c); }, command);
            }
        };
};

class ICatalogue { 
    public:
        class AddProductCommand;
    private:
        virtual void addProduct(const Product& product) = 0;
};

class IInventory {
    public:
        class AddItemsCommand;

    private:
        virtual void addItems(Product product, uint32_t quantity) = 0;
};

class ICatalogue::AddProductCommand: public Command<ICatalogue> {
    public:
        AddProductCommand(Product product): _product { product } {};
        void execute(ICatalogue& catalogue) {
            catalogue.addProduct(_product);
        }
    private:
        Product _product;
};

class IInventory::AddItemsCommand: public Command<IInventory> {
    public:
        AddItemsCommand(Product product, uint32_t quantity): _product {product}, _quantity{quantity} {};
        void execute(IInventory& inventory) {
            inventory.addItems(_product, _quantity);
        }
    private:
        Product _product;
        uint32_t _quantity;
};

class Catalogue: public ICatalogue {
    private:
        void addProduct(const Product& product) override {
            _product.push_back(product);            
        }
        std::vector<Product> _product {};
};

auto productHash = [](const Product& p){ return std::hash<std::string>{}(p.getName()); };

class Inventory: public IInventory {
    private:
        void addItems(Product product, uint32_t quantity) override {
            _inventory[product] += quantity;
        };
        std::unordered_map<Product, uint32_t, decltype(productHash)> _inventory {10, productHash};
};

class Shop: public Catalogue, public Inventory, public MultiReceiver<ICatalogue, IInventory> {
};

int main() {
    Product banana { "Banana" };
    Shop::CommandList commandList {
        std::make_shared<ICatalogue::AddProductCommand>(banana),
        std::make_shared<IInventory::AddItemsCommand>(banana, 12)
    };
    Shop shop;
    shop.applyCommandList(commandList);

    return 0;
}

Solution

  • This has nothing to do with std::visit and also nothing with overload resolution.

    It is name lookup for this->applyCommand itself that fails. Your class has multiple base classes that have distinct applyCommand member functions and so name lookup is ambiguous. Name lookup of class members requires the name to be found unambiguously in one of the base classes only.

    If your intention is to make each of these members of the base classes available for overload resolution in the derived class, then you need to explicitly make them available with using declarations in the derived class:

    using Receiver<Interface>::applyCommand;
    using Receiver<Interfaces>::applyCommand...;
    

    However, it will then fail overload resolution, because none of the overloads are viable. You are trying to pass Command<SomeInterface> to applyCommand, but its parameter expects a Command<Receiver<SomeInterface>>.