Toy Event Store
The snippet can be accessed without any authentication.
Authored by
Gerhard Raven
based on (some of the ideas behind) Louis Dionne's poly library...
snippetfile1.txt 7.87 KiB
#include <typeinfo>
#include <utility>
#include <typeindex>
#include <type_traits>
#include <algorithm>
#include <stdexcept>
#include <optional>
#include <iterator>
void* magic_cast(std::type_index targetType, std::type_index sourceType, void*) { return nullptr; }
struct VTable {
void (*delete_)(void*);
void* (*cast_)(std::type_index, void*);
std::optional<size_t> (*size_)(const void*);
std::type_index (*type_)();
};
template <typename T>
void delete_(void* self) { delete static_cast<T*>(self); }
template <>
void delete_<void>(void* self) { };
template <typename T>
std::type_index type_() { return std::type_index(typeid(T)); }
template <typename T>
void* cast_(std::type_index type, void* self) {
return type==std::type_index(typeid(T)) ? static_cast<T*>(self)
: magic_cast(type,std::type_index(typeid(T)),self);
}
template <>
void *cast_<void>(std::type_index,void*) { return nullptr; }
namespace details {
using std::size;
template <typename T, typename... Args>
constexpr auto size( const T&, Args&&... ) noexcept {
static_assert( sizeof...( Args ) == 0, "No extra args please" );
return std::nullopt;
}
}
template <typename T>
std::optional<size_t> size_(const void* self) { return details::size(*static_cast<const T*>(self)); }
template <>
std::optional<size_t> size_<void>(const void* ) { return std::nullopt; }
template <typename T>
inline constexpr VTable const vtable_for = { &delete_<T>, &cast_<T>, &size_<T>, &type_<T> };
class Data {
// for now, optimize for size. Could put selected functions also 'in-situ' to
// improve optimization (eg. inlining) as
const VTable *m_vtable = &vtable_for<void>;
void* m_ptr = nullptr;
// TODO: add SBO here, so that eg. int, double, vector<T> payloads fit in-situ
// WARNING: SBO will invalidate pointers to payload when resizing the store
// so _only_ do this _after_ we have a store which can be completely
// reserved during initialize, _prior_ to it being populated with content!
// (note: the above actually depends on the container implementation which is
// used to contain 'Data'... if the Data instances inside the container are
// stable then obviously SBO doesn't affect the payload stability ;-)
public:
Data() = default;
template <typename T, // TODO: require T to be move constructible
typename = std::enable_if_t<!std::is_same_v<Data,std::decay_t<T>>>>
Data(T&& t) : m_vtable{ &vtable_for<T> }, m_ptr{ new T{std::move(t)} } {
}
// 'adopt' a unique pointer...
template <typename T>
Data(std::unique_ptr<T,std::default_delete<T>> t) : m_vtable{ &vtable_for<T> }, m_ptr{ t.release() } {
}
~Data() { m_vtable->delete_(m_ptr); }
Data(const Data&) = delete;
Data& operator=(const Data& rhs) = delete;
Data(Data&& rhs) noexcept : m_vtable{ std::exchange(rhs.m_vtable,&vtable_for<void>) },
m_ptr{ std::exchange(rhs.m_ptr,nullptr) } {
}
Data& operator=(Data&& rhs) noexcept {
m_vtable->delete_(m_ptr);
m_vtable = std::exchange(rhs.m_vtable,&vtable_for<void>);
m_ptr = std::exchange(rhs.m_ptr,nullptr);
return *this;
}
friend void swap(Data& lhs, Data& rhs) noexcept {
using std::swap;
swap(lhs.m_vtable,rhs.m_vtable);
swap(lhs.m_ptr,rhs.m_ptr);
}
template <typename T> const T* get_ptr() const {
return static_cast<T*>(m_vtable->cast_(std::type_index(typeid(T)),m_ptr));
}
std::optional<size_t> size() const { return m_vtable->size_(m_ptr); }
std::type_index type() const { return m_vtable->type_(); }
};
#include <vector>
template <typename Key>
class VectorStore {
std::vector<std::pair<Key,Data>> m_store;
public:
VectorStore() = default;
template <typename K>
const Data *find( K&& k ) const {
auto i = std::find_if(m_store.begin(),m_store.end(),
[key=std::forward<K>(k)](const auto& d) { return d.first==key; } );
return i!=m_store.end() ? &i->second : nullptr;
}
template <typename T, typename K>
const auto& emplace(K&& k, T&& t) {
if (find(k)!=nullptr) throw std::runtime_error("entry already exists");
return m_store.emplace_back(std::forward<K>(k), std::forward<T>(t) );
}
};
#include <unordered_map>
template <typename Key>
class UnorderedStore {
std::unordered_map<Key,Data> m_store;
public:
UnorderedStore() = default;
template <typename K>
const Data *find( K&& k ) const {
auto i = m_store.find( std::forward<K>(k) );
return i!=m_store.end() ? &i->second : nullptr;
}
template <typename T, typename K>
const auto& emplace(K&& k, T&& t) {
if ( const auto&[ ret, ok ] = m_store.try_emplace(std::forward<K>(k), std::forward<T>(t) ); ok ) {
return *ret;
}
throw std::runtime_error("entry already exists");
}
};
template <typename Key,template<typename>typename StoreImpl=UnorderedStore>
struct Store : private StoreImpl<Key> {
template <typename T, typename K>
const std::decay_t<T>& put( K&& k, T&& t ) {
auto& d = this->emplace(std::forward<K>(k),std::forward<T>(t));
auto* p = d.second.template get_ptr<std::decay_t<T>>();
return *p;
}
template <typename T, typename K>
const std::decay_t<T>& put( K&& k, std::unique_ptr<T> t ) {
auto& d = this->emplace(std::forward<K>(k),std::move(t));
auto* p = d.second.template get_ptr<std::decay_t<T>>();
return *p;
}
template <typename T, typename K>
const T& get(K&& k) const {
const Data* d = this->find(std::forward<K>(k));
if (!d) throw std::out_of_range{"oops"};
const auto* p = d->get_ptr<T>();
if (!p) throw std::bad_cast{};
return *p;
}
template <typename T,typename K >
const T* get_ptr(K&& k) const {
const Data* d = this->find(std::forward<K>(k));
if (!d) return nullptr;
const auto* p = d->get_ptr<T>();
if (!p) throw std::bad_cast{};
return p;
}
template <typename K>
std::optional<size_t> size(K&& k) const {
const Data* d = this->find(std::forward<K>(k));
if (!d) throw std::out_of_range{"oops"};
return d->size();
}
template <typename K>
const char* type_name(K&& k) const {
const Data* d = this->find(std::forward<K>(k));
if (!d) throw std::out_of_range{"oops"};
return d->type().name();
}
};
struct MyBase {
virtual ~MyBase() = default;
MyBase(int a) : i{a} {}
int i;
};
struct Derived : MyBase {
Derived(int a, int b) : MyBase{a},j{b} {}
int j;
};
#include <string>
#include <memory>
auto make_store() {
auto store = Store<std::string>{};
store.put( "AVector", std::vector<int>{1,2,3,4,5,6,7,8,9} );
[[maybe_unused]] const int& i = store.put( "AnInt", 99 );
store.put( "ADouble", 123.45 );
store.put( "AString", std::string{"Hello World!"} );
store.put<std::string>( "AnotherString", "Goodbye World!" );
[[maybe_unused]] const int& j = store.put( "AUniqueInt", std::make_unique<int>(-9) );
store.put( "Derived", Derived{ 1, 2 } );
store.put<MyBase>( "Base", std::make_unique<Derived>( 3 , 4 ) );
return store;
}
#include <iostream>
int main() {
auto s = make_store();
std::cout << s.get<std::string>("AString") << '\n';
std::cout << s.get<std::string>("AnotherString") << '\n';
std::cout << s.get<int>("AUniqueInt") << '\n';
std::cout << s.get<MyBase>("Base").i << '\n';
std::cout << s.get<Derived>("Derived").j << '\n';
std::cout << s.size("AString").value_or(0) << '\n';
std::cout << s.size("AVector").value_or(0) << '\n';
std::cout << s.size("AUniqueInt").value_or(0) << '\n';
std::cout << s.type_name("AString") << '\n';
std::cout << s.type_name("AUniqueInt") << '\n';
}
-
Pretty neat!
Beware that put must handle duplicate insertion.
Also, cast's signature seems overkill if you aim for exact type equality. In this case, it can be simply
bool(std::type_index)
. Thevoid*
signature is only needed if you want to keep the TES' ability to downcast multiple types intoT*
. -
@raaij: FYI
Please register or sign in to comment