7

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.

Community
  • 1
  • 1
user2296177
  • 2,807
  • 1
  • 15
  • 26
  • 1
    Maybe use a `union`? – Kerrek SB May 07 '16 at 23:31
  • @KerrekSB I understand the premise behind small object optimizations (such as SSO for `std::string`). However, I can't see how a destructor could be called without knowing the type. – user2296177 May 07 '16 at 23:34
  • 2
    @user2296177: the private, type-erased derived class is something like `Impl`; if you know that `sizeof(Impl)` doesn't exceed `sizeof(any)` (plus maybe an extra word or so), then you can construct `Impl` in-place in the object itself. You don't need to allocate memory dynamically in order to create objects dynamically. – Kerrek SB May 07 '16 at 23:48
  • @KerrekSB I assume you mean using something like `std::aligned_storage_t`, but I still need to manually call the destructor, and I can only do that by knowing what type is stored there. Could you please post an example? – user2296177 May 08 '16 at 00:01
  • 2
    You know the type when the contained value is set. Save the destructor in a `std::function` member variable that you call when the `any` is destructed. – Raymond Chen May 08 '16 at 00:06
  • 2
    @user2296177: The destructor is part of the type erasure, right? You just call `ptr()->~Base()`. – Kerrek SB May 08 '16 at 00:40

3 Answers3

1

Typically, any takes anything and dynamically allocates a new object from it:

struct any {
    placeholder* place;

    template <class T>
    any(T const& value) {
        place = new holder<T>(value);
    }

    ~any() {
        delete place;
    }
};

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:

union Storage {
    placeholder* ptr;
    std::aligned_storage_t<sizeof(ptr), sizeof(ptr)> buffer;
};

where we have some template <class T> is_small_object { ... } to decide whether or not we're doing ptr = new holder<T>(value) or new (&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 doing delete ptr or we're doing static_cast<T*>(&buffer)->~T();, the latter of which depends on keeping track of T!

So we introduce our own vtable-like thing. Our any will then hold onto:

enum Op { OP_DESTROY, OP_TYPE_INFO };
void (*vtable)(Op, Storage&, const std::type_info* );
Storage storage;

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 a union...) and you don't want to just bloat your any 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 the vtable:

template <class T,
          class dT = std::decay_t<T>,
          class V = VTable<dT>,
          class = std::enable_if_t<!std::is_same<dT, any>::value>>
any(T&& value)
: vtable(V::vtable)
, storage(V::create(std::forward<T>(value))
{ }

where our VTable types are something like:

template <class T>
struct PolymorphicVTable {
    template <class U>
    static Storage create(U&& value) {
        Storage s;
        s.ptr = new holder<T>(std::forward<U>(value));
        return s;
    }

    static void vtable(Op op, Storage& storage, const std::type_info* ti) {
        placeholder* p = storage.ptr;

        switch (op) {
        case OP_TYPE_INFO:
            ti = &typeid(T);
            break;
        case OP_DESTROY:
            delete p;
            break;
        }
    }
};

template <class T>
struct InternalVTable {
    template <class U>
    static Storage create(U&& value) {
        Storage s;
        new (&s.buffer) T(std::forward<U>(value));
        return s;
    }

    static void vtable(Op op, Storage& storage, const std::type_info* ti) {
        auto p = static_cast<T*>(&storage.buffer);

        switch (op) {
        case OP_TYPE_INFO:
            ti = &typeid(T);
            break;
        case OP_DESTROY:
            p->~T();
            break;
        }
    }
};

template <class T>
using VTable = std::conditional_t<sizeof(T) <= 8 && std::is_nothrow_move_constructible<T>::value,
                   InternalVTable<T>,
                   PolymorphicVTable<T>>;

and then we just use that vtable to implement our various operations. Like:

~any() {
    vtable(OP_DESTROY, storage, nullptr);
}
Barry
  • 286,269
  • 29
  • 621
  • 977
0

How can std::any avoid dynamic allocation and still destroy such values if no compile time information is known at the time of destruction

That seems like a loaded question. The latest draft requires this constructor:

template <class ValueType> any(ValueType &&value);

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

template <typename T>
  struct IsSmallObject : ...type_traits...

In the former case, you can have a pointer to your uninitialized storage:

union storage
{
    void* ptr;
    typename std::aligned_storage<3 * sizeof(void*), 
                std::alignment_of<void*>::value>::type buffer;
};

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:

  template <typename T>
  struct SmallHandler
  {
    // ...

    static void destroy(any & bye)
    {
        T & value = *static_cast<T *>(static_cast<void*>(&bye.storage.buffer));
        value.~T();
        this.handle = nullptr;
    }

    // ...
   };

Then the any class:

// Note, we don't need to know `T` here!
class any
{
  // ...

  void clear() _NOEXCEPT
  {
    if (handle) this->call(destroy);
  }

  // ...
  template <class>
  friend struct SmallHandler;
};

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:

  1. nothrow_move_constructible
  2. sizeof(T) <= sizeof(storage). In my case this is 3 * sizeof(void*)
  3. alignof(T) <= alignof(storage). In my case this is std::alignment_of<void*>::value
-1

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)

#include <iostream>
#include <type_traits>

using std::cout;
using std::endl;

struct A { ~A() { cout << "~A" << endl; }};
struct B { ~B() { cout << "~B" << endl; }};

struct Base_holder {
  virtual ~Base_holder() {}
};

template <class T>
struct Holder : Base_holder {
  T value_;

  Holder(T val) : value_{val} {}
};

struct Any {  
  std::aligned_storage_t<64> buffer_;
  Base_holder* p_;

  template <class T>
  Any(T val)
  {
    p_ = new (&buffer_) Holder<T>{val};
  }

  ~Any()
  {
    p_->~Base_holder();
  }
};

auto main() -> int
{  
  Any a(A{});
  Any b(B{});

  cout << "--- Now we exit main ---" << endl;
}

The output:

~A
~A
~B
~B
--- Now we exit main ---
~B
~A

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 and Holder. We initialize them via placement new in a std::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.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • @Barry of course. That's why you would actually have an union. For this and for a dynamically allocated memory. – bolov May 08 '16 at 02:54
  • Strictly speaking, `static_cast(static_cast(&buffer_))` is UB. The "pointer-to-structure is a pointer to its first subobject" is guaranteed only for standard-layout types, which `Holder` is not. – Ben Voigt May 08 '16 at 03:57
  • @BenVoigt the casts has nothing to do with subobject as far as I know. I used `buffer_` as a storage for `Holder`. I construct with placement new into it, and then I call the destructor via the base class `Base_holder` and I use pointers to have polymorphism. I am not that familiar with the standard, but it looks ok to me. – bolov May 08 '16 at 05:14
  • @BenVoigt should I post a question about this specific matter? – bolov May 08 '16 at 05:16
  • @bolov: You use placement `new` to construct a `Holder`, which ought to be aligned at the beginning of `buffer_`. But later you create a pointer to `Base_holder` without performing the appropriate upcast. There's no guarantee that the `Base_holder` subobject starts at the first byte of the `Holder` object (the Standard explicitly defines when such a guarantee is made -- for objects with standard-layout type -- and `Holder` is not standard-layout). – Ben Voigt May 08 '16 at 05:51
  • @BenVoigt I see what you are saying. Ty, I am very happy to learn this. I think I came up with a solution. Would you please check the edited post to see if it is ok? Basically I now have a `Base_holder` pointer. – bolov May 08 '16 at 06:20
  • This should at least `static_assert` that you have enough space. On a typical 64-bit system, the vtable pointer itself takes up 8 bytes. – T.C. May 08 '16 at 06:24
  • @T.C. the point was to illustrate the concept. Of course you would have to check if it fits and if `T` is `no_throw_move_constructible` – bolov May 08 '16 at 06:27
  • @bolov: Yes, that's a very simple method for fixing the problem. It does take up extra storage, but I haven't been able to think of a good way to avoid that. I guess it should be possible to have just one pointer (to a template-produced static deleter function) rather than a pointer and a v-table. – Ben Voigt May 08 '16 at 07:58