Why no "variant" any in boost or Standard?

c++, c++11

Solution

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?

Problem

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?

Original source