One advantage of any
over variant
is, that one does not need to specify all types, that it may contain. I've noticed, that as the number of types a variant
may contain grows, people tend to switch to any
at some point, because they simply don't keep track of all the types anymore. I think a hybrid between any
and variant
is possible. One could store the "placeholder
" (via placement new
) of any
in aligned_storage
, with the size calculated in a constexpr
function or template metafunction, from a sample of the largest types, that may end up being stored. The user, on the other hand, would not need to specify all the types, that an any
might contain. The any
could also throw at any time, if the user would try to store something larger than the aligned_storage
in there.
Does such a "variant_any
" class exist? Is there some inherent problem with the idea?
Here is a basic some
.
The T
copy/assign/move/etc can be implemented in terms of emplace
. SFINAE using can_store<T>
can ensure that only types the some
can actually store are assignable to it, avoiding needless exceptions.
Currently, moving from some
destroys its contents instead of just moving from it. And a some
can be empty (they are "nulllable").
load_from
is a 'can-fail' copy constructor from another some
-- it returns false
on failure. I could add a 'cannot-fail' from a smaller some
(even a copy/assignment operator) to complete it.
some_meta
is a manual virtual function table. One exists per type T
you store in a some
of any size. It stores the type-erased operations on the type T
that some
wants to use (in this case, copy move and destroy), plus some data about the type (size, alignment and type identity). It could be augmented with additional operations like comparison and serialization. For binary operations, logic to handle "no matching type" has to be considered. For stuff like serialization, I'd have it call the free function serialize
and deserialize
on the T
. In both cases, we impose additional requirements on what some
can store (you can, with a bit of work, handle "maybe serialize", but that gets messy).
You could even imagine a system where you can store a set of operations to perform on the data (binary and unary) and pass said operations bundled in types passed to some. At this point, we are approaching boost
's type erasure library, however.
namespace details {
template<std::size_t Size, std::size_t Align=0>
struct storage_helper {
using type = std::aligned_storage_t<Size, Align>;
enum { alignment = alignof(type), size = Size };
};
template<std::size_t Size>
struct storage_helper<Size, 0> {
using type = std::aligned_storage_t<Size>;
enum { alignment = alignof(type), size = Size };
};
template<std::size_t size, std::size_t align>
using storage_helper_t = typename storage_helper<size,align>::type;
template<class T>using type=T;
struct some_meta {
type<void(void*)>* destroy;
type<void(void* dest, void const* src)>* copy;
type<void(void* dest, void* src)>* move;
std::type_index type;
size_t size;
size_t align;
template<class T> static some_meta const* get() {
static const some_meta retval( create<T>() );
return &retval;
};
private:
template<class T> static some_meta create() {
return {
[](void* p){ ((T*)p)->~T(); },
[](void* out, void const* in){ new(out)T(*(T*)in); },
[](void* dest, void* src) { new(dest)T(std::move(*(T*)src)); },
typeid(T),
sizeof(T),
alignof(T)
};
}
};
}
template<class>struct emplace_as{};
template< std::size_t size, std::size_t Align=0 >
struct some {
enum { align = details::storage_helper<size, Align>::alignment };
using data_type = details::storage_helper_t<size, Align>;
template<size_t, size_t> friend struct some;
template<class T> struct can_store :
std::integral_constant< bool, ((align%alignof(T))==0) && sizeof(T) <= size) >
{};
template<size_t x, size_t a>
static bool can_fit( some<x,a> const& o ) {
if (x<=size && ((align%some<x,a>::align)==0)) return true; // should cause optimizations
if (!o.meta) return true;
if (o.meta->size > size) return false;
if (o.meta->align > align) return false;
return true;
}
private:
data_type data;
details::some_meta const* meta = nullptr;
public:
// true iif we are (exactly) a T
template<class T>
bool is() const {
return meta && (meta->type == typeid(T));
}
explicit operator bool()const { return meta!=nullptr; }
template<class T>
T* unsafe_get() { return reinterpret_cast<T*>(&data); }
template<class T>
T* get() { if (is<T>()) return unsafe_get<T>(); else return nullptr; }
void clear() { if (meta) meta->destroy(&data); meta = nullptr; }
template<class T, class... Args>
std::enable_if_t< can_store<T>{} >
emplace(Args&&...args) {
clear();
new(&data) T(std::forward<Args>(args)...);
meta = details::some_meta::get<T>();
}
some()=default;
some(some const& o) {
*this = o;
}
some(some const&&o):some(o){}
some(some&o):some(const_cast<some const&>(o)){}
some(some&& o) {
*this = std::move(o);
}
some& operator=(some const&o) {
if (this == &o) return *this;
clear();
if (o.meta) {
o.meta->copy( &data, &o.data );
meta=o.meta;
}
return *this;
}
some& operator=(some &&o) {
if (this == &o) return *this;
clear();
if (o.meta) {
o.meta->move( &data, &o.data );
meta=o.meta;
o.clear();
}
return *this;
}
some& operator=(some const&&o) { return *this=o; }
some& operator=(some &o) { return *this=const_cast<some const&>(o); }
// from non-some:
template<class T,class=std::enable_if_t<can_store<std::decay_t<T>>{}>>
some(T&& t){
emplace<std::decay_t<T>>(std::forward<T>(t));
}
template<class T, class...Args,class=std::enable_if_t<can_store<T>{}>>
some( emplace_as<T>, Args&&...args ){
emplace<T>(std::forward<Args>(args)...);
}
template<class T,class=std::enable_if_t<can_store<std::decay_t<T>>{}>>
some& operator=(T&&t){
emplace<std::decay_t<T>>(std::forward<T>(t));
return *this;
}
template<size_t x, size_t a>
bool load_from( some<x,a> const& o ) {
if ((void*)&o==this) return true;
if (!can_fit(o)) return false;
clear();
if (o.meta) {
o.meta->copy( &data, &o.data );
meta=o.meta;
}
return true;
}
template<size_t x, size_t a>
bool load_from( some<x,a> && o ) {
if ((void*)&o==this) return true;
if (!can_fit(o)) return false;
clear();
if (o.meta) {
o.meta->move( &data, &o.data );
meta=o.meta;
o.clear();
}
return true;
}
~some() { clear(); }
};
template<class T, class...Ts>
using some_that_fits = some< (std::max)({sizeof(T),sizeof(Ts)...}), (std::max)({alignof(T),alignof(Ts)...}) >;
the meta
object is a manually implemented virtual function table, basically. It reduces the memory overhead of a given some
to one pointer (above its storage buffer).
live example
As demonstrated above, it is quite viable.
Note that create
returns a pointer to the same meta
for the same type T
, even if called more than once.
I have exercised about half the code paths in my test above. The others probably have bugs.
some_that_fits
lets you pass a set of types, and it returns a some
type that fits those types.
No exceptions, other than those generated by the operations on the stored types by said stored types, are thrown. When possible, I test at compile time to ensure types fit.
I could possibly add support for greater alignment, small storage types by starting them at an offset into my data?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With