Search code examples
c++types

Extending type erasure


I am working on a software which makes heavy use of type erasure. This allows duck typing. For now, let us assume that I have something that can fly and that can print. How can I require flying and printing at the same time? I am limited to C++17 and using concepts is not an option.

For example, consider the following implementation for flying:

#include <iostream>
#include <memory>
#include <type_traits>
class FlyingConcept {
  public:
    virtual ~FlyingConcept() = default;
    virtual void fly() = 0;
};

template <typename T>
class FlyingModel : public FlyingConcept {
  public:
    FlyingModel(T t) : t{std::forward<std::decay_t<T>>(t)} {}
    void fly() override {t.fly();}
  private:
    T t;
};

class AnyFlyer {
  public:
    template <typename T>
    AnyFlyer(T t) : flyingConcept{std::make_unique<FlyingModel<std::decay_t<T>>>(std::forward<std::decay_t<T>>(t))} {}
    void fly() {flyingConcept->fly();}
  private:
    std::unique_ptr<FlyingConcept> flyingConcept;
};

When I want to make something fly, I can use the following generic method and invoke it with anything that implements a fly method:

void doFly(AnyFlyer flyer)
{
  flyer.fly();
}

struct OnlyFlying {
  void fly() { std::cout << "can fly" << std::endl;}
};

class BothFlyingAndPrinting {
  public:
    void fly() {std::cout << "can fly (and print)" << std::endl;}
    void print () {std::cout << "can print (and fly)" << std::endl;}
};

int main() {
  doFly(OnlyFlying{});            // prints "can fly" 
  doFly(BothFlyingAndPrinting{}); // prints "can fly (and print)"
}

Printing is implemented in a similar fashion:

class PrintableConcept {
  public:
    virtual ~PrintableConcept() = default;
    virtual void print() = 0;
};

template <typename T>
class PrintableModel : public PrintableConcept {
  public:
    PrintableModel(T t1) : t{std::forward<std::decay_t<T>>(t1)} {};
    void print() override { t.print();};
  private:
    std::decay_t<T> t;
};

class AnyPrintable {
  public:
    template<typename T>
    AnyPrintable(T t) : printableConcept{std::make_unique<PrintableModel<std::decay_t<T>>>(std::forward<std::decay_t<T>>(t))}  {}
    void print() {printableConcept->print();}
  private:
    std::unique_ptr<PrintableConcept> printableConcept;
};

struct OnlyPrinting {
  void print() {std::cout << "can print" << std::endl;}
};

void doPrint(AnyPrintable printable)
{
  printable.print();
}

int main() {
  doPrint(OnlyPrinting{});          // prints "can print"
  doPrint(BothFlyingAndPrinting{}); // prints "can print (and fly)"
}

I need something that can both print and fly at the same time, which only BothFlyingAndPrinting satisfy. I can always reimplement it from scratch, but I do not want to copy/paste that much code. Is there a simple way of "extending" types with type erasure? If I used multiple inheritance (yuck) on both FlyingConcept and PrintingConcept, I would still have to implement that Model.


Solution

  • You can certainly reduce the amount of boilerplate in your definitions

    class AnyBase {
      public:
        template <typename T>
        AnyBase(T&& t) : ptr(new std::decay_t<T>(std::forward<T>(t)), &deleter<std::decay_t<T>>) {}
      protected:
        std::unique_ptr<void, void(*)(void*)> ptr;
      private:
        template <typename T>
        static void deleter(void* ptr) { delete static_cast<T*>(ptr); }
    };
    
    class AnyFlyer : public virtual AnyBase {
      public:
        template <typename T>
        AnyFlyer(T&& t) : AnyBase(std::forward<T>(t)), do_fly(&do_fly_impl<std::decay_t<T>>) {}
        void fly() { do_fly(ptr.get()); }
      private:
        void(*do_fly)(void*);
        template <typename T>
        static void do_fly_impl(void* ptr) { return static_cast<T*>(ptr)->fly(); }
    };
    
    class AnyPrintable : public virtual AnyBase {
      public:
        template <typename T>
        AnyPrintable(T&& t) : AnyBase(std::forward<T>(t)), do_print(&do_print_impl<std::decay_t<T>>) {}
        void print() { do_print(ptr.get()); }
      private:
        void(*do_print)(void*);
        template <typename T>
        static void do_print_impl(void* ptr) { return static_cast<T*>(ptr)->print(); }
    };
    
    class AnyPrintableFlyer : public virtual AnyPrintable, public virtual AnyFlyer {
      public:
        template <typename T>
        AnyPrintableFlyer(T&& t) : AnyBase(std::forward<T>(t)), AnyPrintable(std::forward<T>(t)), AnyFlyer(std::forward<T>(t)) {}
    };
    

    See it on coliru

    N.b. it is safe to std::forward the value to multiple virtual base classes, because only AnyBase will move from it.