Search code examples
c++ooptemplatesc++20

C++ Passing an implementation of a templated interface as a parameter


Is it possible to create a method that accepts a concrete implementation of a templated interface?

To illustrate the problem I created a minimal example. See produce(ISource<IProduct>* source) below:

// Product
class IProduct {
public:
    virtual ~IProduct() = default;
};
class Bread : public IProduct {};
class Cucumber : public IProduct {};

// Source
template<typename T>
class ISource {
public:
    virtual T* get() = 0;
};

class Bakery : public ISource<Bread> {
public:
    Bread* get() override {
        return new Bread();
    }
};

class Grocery : public ISource<Cucumber> {
public:
    Cucumber* get() override {
        return new Cucumber();
    }
};

// Producer
class IProducer {
public:
    virtual IProduct* produce(ISource<IProduct>* source) = 0;
};

class Shop : public IProducer {
public:
    IProduct* produce(ISource<IProduct>* source) override {
        return source->get();
    }
};

My attempts:

template<typename T>
T* test(IProducer* producer, ISource<T>* source) {
    // (1) Cannot initialize a parameter of type 'ISource<IProduct> *' with an lvalue of type 'ISource<Bread> *'
    T* product = dynamic_cast<T *>(producer->produce(source));
    return product;
}

int main(int argc, char** argv) {

    auto bakery = new Bakery();
    auto shop = new Shop();

    // FAILS: see (1)
    Bread* bread1 = test(shop, bakery);
}

Solution

  • ISource<IProduct> and ISource<Bread> are two completely separate and unique types, and cannot be cast to one another, and you cannot pass one where the other is expected. (unless you define a conversion yourself).

    you can however erase their different types using type-erasure, and only keep the fact that they can both produce IProduct*

    the C++ way to do this using the standard library is using std::function

    #include <functional>
    
    // Product
    class IProduct {
    public:
        virtual ~IProduct() = default;
    };
    class Bread : public IProduct {};
    
    // Source
    template<typename T>
    class ISource {
    public:
        virtual T* get() = 0;
    };
    
    class Bakery : public ISource<Bread> {
    public:
        Bread* get() override {
            return new Bread();
        }
    };
    
    // Producer
    class IProducer {
    public:
        virtual IProduct* produce(std::function<IProduct*()> source) = 0;
    };
    
    class Shop : public IProducer {
    public:
        IProduct* produce(std::function<IProduct*()> source) override {
            return source();
        }
    };
    
    template<typename T>
    T* test(IProducer* producer, ISource<T>* source) {
        T* product = dynamic_cast<T*>(producer->produce([&]()->IProduct* {return source->get(); }));
        return product;
    }
    
    int main(int argc, char** argv) {
    
        auto bakery = new Bakery();
        auto shop = new Shop();
    
        Bread* bread1 = test(shop, bakery);
    }
    

    type-erasure aside, you can have ISource not be templated and use covariant return types.

    class ISource {
    public:
        virtual IProduct* get() = 0;
    };
    
    class Bakery : public ISource {
    public:
        Bread* get() override {
            return new Bread();
        }
    };
    
    class Grocery : public ISource {
    public:
        Cucumber* get() override {
            return new Cucumber();
        }
    };
    
    // Producer
    class IProducer {
    public:
        virtual IProduct* produce(ISource* source) = 0;
    };
    
    class Shop : public IProducer {
    public:
        IProduct* produce(ISource* source) override {
            return source->get();
        }
    };
    
    template<typename source_t>
    auto test(IProducer* producer, source_t* source) {
        auto product = dynamic_cast<decltype(source->get())>(producer->produce(source));
        return product;
    }