The standard working draft (n4582, 20.6.3, p.552) states the following suggestion for implementations of std::any
:
Implementations should avoid the use of dynamically allocated memory for a small contained object. [ Example: where the object constructed is holding only an int. —end example ] Such small-object optimization shall only be applied to types T for which is_nothrow_move_constructible_v is true.
As far as I know, std::any
can be easily implemented through type erasure/virtual functions and dynamically allocated memory.
How can std::any
avoid dynamic allocation and still destroy such values if no compile time information is known at the time of destruction; how would a solution that follows the standard's suggestion be designed?
If anyone wants to see a possible implementation of the non-dynamic part, I've posted one on Code Review: https://codereview.stackexchange.com/questions/128011/an-implementation-of-a-static-any-type
It's a little too long for an answer here. It's based on the suggestions of Kerrek SB on the comments below.
Inspired by boost any I came up with this (test it on ideone) (I created a minimal case to show how to destroy a type erased container like
any
without dynamic memory. I focused only on constructor/destructor, omitting everything else, ignoring move semantics and other things)The output:
Of course the first one are temporaries being destroyed the last two prove that destruction of
Any
calls the right destructor.The trick is to have polymorphism. That's why we have
Base_holder
andHolder
. We initialize them via placement new in astd::aligned_storage
and we explicitly call the destructor.This is just to prove you can call the right destructor without knowing the type held by
Any
. Of course in a real implementation you would have an union for this or a pointer to a dynamically allocated memory and a boolean telling you which one you have.That seems like a loaded question. The latest draft requires this constructor:
I can't think of why you need to have "type erasure" unless you want code to handle both small and large cases at the same time. But then why not have something like this?1
In the former case, you can have a pointer to your uninitialized storage:
Using a union as @KerrekSB suggested.
Notice that the type is not needed to be known for the storage class. Using some kind of handle/dispatch (not sure about the real name of the idiom) system it becomes trivial at this point.
First let's tackle what destructing would look like:
Then the
any
class:Here we factor out the logic that needs to know the compile-time type to a handler/dispatch system, while the bulk of the
any
class only has to deal with RTTI.1: Here's the conditions I would check for:
nothrow_move_constructible
sizeof(T) <= sizeof(storage)
. In my case this is3 * sizeof(void*)
alignof(T) <= alignof(storage)
. In my case this isstd::alignment_of<void*>::value
Typically,
any
takes anything and dynamically allocates a new object from it:We use the fact that
placeholder
is polymorphic to handle all of our operations - destruction, cast, etc. But now we want to avoid allocation, which means we avoid all the nice things that polymorphism gives us - and need to reimplement them. To start with, we'll have some union:where we have some
template <class T> is_small_object { ... }
to decide whether or not we're doingptr = new holder<T>(value)
ornew (&buffer) T(value)
. But construction isn't the only thing we have to do - we also have to do destruction and type info retrieval, which look different depending on which case we're in. Either we're doingdelete ptr
or we're doingstatic_cast<T*>(&buffer)->~T();
, the latter of which depends on keeping track ofT
!So we introduce our own vtable-like thing. Our
any
will then hold onto:You could instead create a new function pointer for each op, but there are probably several other ops that I'm missing here (e.g.
OP_CLONE
, which might call for changing the passed-in argument to be aunion
...) and you don't want to just bloat yourany
size with a bunch of function pointers. This way we lose a tiny bit of performance in exchange for a big difference in size.On construction, we then populate both the
storage
and thevtable
:where our
VTable
types are something like:and then we just use that vtable to implement our various operations. Like: