Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why no "variant" any in boost or Standard?

Tags:

c++

c++11

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?

like image 390
user1095108 Avatar asked Nov 27 '14 14:11

user1095108


1 Answers

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?

like image 110
Yakk - Adam Nevraumont Avatar answered Oct 06 '22 01:10

Yakk - Adam Nevraumont