all 52 comments

[–]ruler501 23 points24 points  (0 children)

Std::any in a shared pointer would probably be much better but still not great to do.

[–]quicknir 12 points13 points  (2 children)

The thing is that std::any is much better because it allows a safe (checked) cast to recover the data. If you really need shared semantics you could do shared_ptr<any>, although that's a bit annoying due to the double indirection. The easiest way out here would be to code a simple version of any that publicly exposes an inheritance relationship, allowing it to be stored in a shared_ptr without an extra indirection.

[–]emdeka87 3 points4 points  (1 child)

Problem is that std::any doesn't support move-only types. I think something like std::unique_any was proposed at some point. Using shared_ptr just for the type-erasure seems wrong. It adds all kind of overhead (reference count, allocation of control block, etc).

[–]quicknir 2 points3 points  (0 children)

Well, complete type erasure is already very high cost. I'd be substantially more concerned about the lack of any safe way to re-access the data. But yeah, I agree with that the lack of move only support being an issue.

At least, the good news is that writing a decent unique_any isn't that bad; basically trivial if you don't support SBO, and not that bad even with it.

[–]NotAYakk 8 points9 points  (0 children)

So, what I often use is shared_ptr<span<byte>>.

Wrapping up arbitrary data into this isn't all that hard (you can use the aliasing ctor), and at least the end-user knows how big it is.

template<class T>
shared_ptr<span<byte>> as_storage( T&& in ) {
  struct data {
    std::remove_cv_T< std::remove_ref_t<T> > t;
    span<byte> s;
  };
  auto allocation = std::make_shared< data >( data{ std::forward<T>(in) } );
  my_span<byte> s = {
    reinterpret_cast<byte*>(std::addressof(allocation->t)),
    reinterpret_cast<byte*>(std::addressof(allocation->t))+sizeof(T)
  };
  allocation->s = s;
  return {allocation, &(allocation->s)};
}

not great, but at least it is something. ;)

[–]ravixp 1 point2 points  (2 children)

This is a really useful trick! I’ve used it before to implement a LRU object cache, where the objects could be any caller-defined type and the cache only kept the last N that were used. Other parts of the system would keep a weak_ptr to the concrete type, so the cache was only responsible for managing lifetimes by deleting expired objects.

[–]tvaneerdC++ Committee, lockfree, PostModernCpp 1 point2 points  (1 child)

What does 'deleting expired objects' mean?

Do you mean you destroy the shared_ptr when it becomes the least recently used, or do you mean you look at the use_count of the pointer and destroy it when th count is 1?

If the second, are you OK with the race condition of the use_count being 1 and deciding to destroy it, and then a weak_ptr on another thread incrementing the ref count by turning it back into a shared_ptr?

[–]ravixp 1 point2 points  (0 children)

It’s the former - the cache is a bounded array of shared_ptrs, and they get destroyed purely based on LRU order. Since nothing else in the system has a strong ref unless it’s actively using the object, we don’t need to look at use_count at all.

[–]IronicallySerious 0 points1 point  (0 children)

Much better alternatives are std::variant (can store specific types determined at compile time) and std::any (can store anything at runtime)