Created
November 10, 2022 23:16
-
-
Save patstew/e068620aab50a92bc91c47de57051ba1 to your computer and use it in GitHub Desktop.
Duck typing for C++
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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