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.
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)) {}
};
N.b. it is safe to std::forward
the value to multiple virtual base classes, because only AnyBase
will move from it.