Skip to content

Instantly share code, notes, and snippets.

@patstew
Created November 10, 2022 23:16
Show Gist options
  • Save patstew/e068620aab50a92bc91c47de57051ba1 to your computer and use it in GitHub Desktop.
Save patstew/e068620aab50a92bc91c47de57051ba1 to your computer and use it in GitHub Desktop.
Duck typing for C++
#include <cassert>
#include <cstddef>
#include <type_traits>
#include <utility>
// Duck typing for C++
namespace duck {
template <template <class> class I> class ref;
template <template <class> class I> class value;
template <template <class> class I, class T> class top;
template <template <class> class I, class T> class base : public I<void> {
void *_duck_ptr = nullptr;
virtual ~base() {}
friend class I<T>;
friend class top<I, T>;
protected:
T *that() { return static_cast<T *>(_duck_ptr); }
const T *that() const { return static_cast<const T *>(_duck_ptr); }
};
template <template <class> class I> class base<I, void> {
virtual ~base() {}
virtual ref<I> _duck_ref() const = 0;
virtual value<I> _duck_value() const = 0;
friend class I<void>;
friend class ref<I>;
friend class value<I>;
template <template <class> class J> friend class empty;
protected:
I<void> *that() const { return assert(false), nullptr; }
};
template <template <class> class I, class T> class top : public I<T> {
template <class U> top(U *v) {
base<I, T>::_duck_ptr = const_cast<void *>(static_cast<const void *>(v));
}
virtual ref<I> _duck_ref() const {
return ref<I>(*static_cast<T *>(base<I, T>::_duck_ptr));
}
virtual value<I> _duck_value() const {
return value<I>(*static_cast<T *>(base<I, T>::_duck_ptr));
}
friend class ref<I>;
friend class value<I>;
};
template <template <class> class I> class storage;
template <template <class> class I> class empty final : public base<I, void> {
void *pad;
empty() {}
virtual ref<I> _duck_ref() const { return ref<I>(); }
virtual value<I> _duck_value() const { return value<I>(); }
friend class storage<I>;
};
template <template <class> class I> class storage {
protected:
alignas(empty<I>) unsigned char buffer[sizeof(empty<I>)];
storage() {}
storage(int) {
check<empty<I>>();
new (&buffer) empty<I>();
}
I<void> *interface() {
return static_cast<I<void> *>(static_cast<void *>(&buffer));
}
const I<void> *interface() const {
return static_cast<const I<void> *>(static_cast<const void *>(&buffer));
}
template <class T> static constexpr void check() {
static_assert(sizeof(buffer) >= sizeof(T), "Buffer in duck wrong size");
static_assert(alignof(empty<I>) >= alignof(T), "Alignment in duck too small");
}
public:
I<void> *operator->() { return interface(); }
I<void> *operator->() const { return interface(); }
};
template <template <class> class I, class T>
using is_not_duck =
std::enable_if_t<!std::is_base_of_v<storage<I>, std::decay_t<T>>>;
template <template <class> class I> class ref : storage<I> {
using storage<I>::buffer;
using storage<I>::interface;
ref() : storage<I>(0) {}
friend class empty<I>;
public:
template <class U, class = is_not_duck<I, U>> ref(U &&v) {
using T = std::remove_reference_t<U>;
storage<I>::template check<top<I, T>>();
new (&buffer) top<I, T>(&v);
}
~ref() { static_cast<base<I, void> *>(interface())->~base<I, void>(); }
ref(ref &&other) : ref() { *this = std::move(other); }
ref &operator=(ref &&other) {
std::swap(buffer, other.buffer);
return *this;
}
ref(const ref &other) : ref(other.interface()->_duck_ref()) {}
ref &operator=(const ref &other) {
return *this = other.interface()->_duck_ref();
}
using storage<I>::operator->;
};
template <template <class> class I> class value : storage<I> {
using storage<I>::buffer;
using storage<I>::interface;
template <class T> struct value_top final : top<I, T> {
using top<I, T>::top;
~value_top() { delete base<I, T>::that(); }
};
value() : storage<I>(0) {}
friend class empty<I>;
public:
template <class U, class = is_not_duck<I, U>> value(U &&v) {
using T = std::decay_t<U>; // We're making a new one so we can remove const
storage<I>::template check<value_top<T>>();
new (&buffer) value_top<T>(new T(std::forward<U>(v)));
}
#if __cplusplus >= 201703L
template <class T, class... Args>
explicit value(std::in_place_type_t<T>, Args &&...args) {
storage<I>::template check<value_top<T>>();
new (&buffer) value_top<T>(new T(std::forward<Args>(args)...));
}
#endif
~value() { static_cast<base<I, void> *>(interface())->~base<I, void>(); }
value(value &&other) : value() { *this = std::move(other); }
value &operator=(value &&other) {
std::swap(buffer, other.buffer);
return *this;
}
value(const value &other) : value(other.interface()->_duck_value()) {}
value &operator=(const value &other) {
return *this = other.interface()->_duck_value();
}
operator ref<I>() const { return interface()->_duck_ref(); }
using storage<I>::operator->;
};
} // namespace duck
// Example code
#if 1
#include <iostream>
struct Bread {};
// Declare an interface for ducks
template <class T> class Duck : public duck::base<Duck, T> {
using duck::base<Duck, T>::that; // Boilerplate
public:
// Ducks can quack and eat bread
// To declare an interface function, we need to write a trivial implementation that forwards the call to "that()"
virtual int quack() const { return that()->quack(); }
virtual void eat(Bread b) { return that()->eat(b); }
};
// A type of duck, that does not inherit from Duck
struct Mallard {
int quack() const { std::cout << "Mallard quacks 3 times" << std::endl;return 3; }
void eat(Bread b) { std::cout << "Mallard ate bread" << std::endl; }
~Mallard() { std::cout << "Mallard died" << std::endl; }
};
// A function taking a Duck
void number_of_quacks(duck::ref<Duck> d) {
int q = d->quack(); // Function calls look normal
std::cout << "This duck quacks " << q << " times" << std::endl;
}
struct Food {
Food(Bread) {}
};
struct Human {
void say(const char* words) const { std::cout << "Human says " << words << std::endl; }
void eat(Food) { std::cout << "Human ate food" << std::endl; }
};
// If we want to treat a human like a Duck we can specialise
template <> class Duck<Human> : public duck::base<Duck, Human> {
using duck::base<Duck, Human>::that;
public:
virtual int quack() const { that()->say("quack"); return 1; }
virtual void eat(Bread& b) { return that()->eat(b); }
};
int main() {
Mallard m;
// This function can be called with anything that matches Duck, and it's checked at compile time
number_of_quacks(m);
// I can make value types of any Duck
duck::value<Duck> d{Mallard{}};
number_of_quacks(d);
// I can reassign to a different type
d = Human();
number_of_quacks(d);
auto d2 = d; // Copy by value
// Construct in place
d = duck::value<Duck>{std::in_place_type<Mallard>};
// d2 is stall a Human underneath
number_of_quacks(d2);
return 0;
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment