Search code examples
c++type-erasureself-destruction

How are self-destructing type erasure classes like std::function implemented?


I want to understand how the implementation of std::function works. For simplicity, let's consider move-only functions with no arguments.

I understand that std::function erases the type of its target through typical type erasure techniques:

template<class Result>
struct function
{
  public:
    template<class Function>
    function(Function&& f)
      : f_(std::make_unique<callable_base>(std::forward<Function>(f)))
    {}

    // XXX how to implement constructor with allocator?
    template<class Alloc, class Function>
    function(const Alloc& alloc, Function&& f);

    Result operator()() const
    {
      return (*f_)();
    }

  private:
    struct callable_base
    {
      virtual Result operator()() const = 0;
      virtual ~callable_base(){}
    };

    template<class Function>
    struct callable
    {
      mutable Function f;

      virtual Result operator()() const
      {
        return f;
      }
    };

    // XXX what should the deleter used below do?
    struct deleter;

    std::unique_ptr<callable_base, deleter> f_;
};

I'd like to extend the functionality of this type to support custom allocation. I'll need to erase the type of the allocator, but that's difficult to do with the use of the std::unique_ptr. The custom deleter given to the unique_ptr needs to know the concrete type of the Function given to the constructor to be able to properly deallocate its storage. I could use another unique_ptr to type erase the deleter, but that solution is circular.

It seems like the callable<Function> needs to deallocate itself. What's the correct way to do that? If I deallocate inside of callable<Function>'s destructor, that seems too early because its members are still alive.


Solution

  • std::function lost its allocators in C++17 in part because of problems with type erased allocators. However, the general pattern is to rebind the allocator to whatever type you're using to do the type erasure, store the original allocator in the type erased thing, and rebind the allocator again when deleting the type erased thing.

    template<class Ret, class... Args>
    struct Call_base {
        virtual Ret Call(Args&&...);
        virtual void DeleteThis();
    protected:
        ~Call_base() {}
    };
    
    template<class Allocator, class Fx, class Ret, class... Args>
    struct Call_fn : Call_base<Ret, Args...> {
        Allocator a;
        decay_t<Fx> fn;
    
        Call_fn(Allocator a_, Fx&& fn_)
            : a(a_), fn(forward<Fx>(fn_))
            {}
    
        virtual Ret Call(Args&& vals) override {
            return invoke(fn, forward<Args>(vals)...);
        }
        virtual void DeleteThis() override {
            // Rebind the allocator to an allocator to Call_fn:
            using ReboundAllocator = typename allocator_traits<Allocator>::
                template rebind_alloc<Call_fn>;
            ReboundAllocator aRebound(a);
            allocator_traits<ReboundAllocator>::destroy(aRebound, this);
            aRebound.deallocate(this, 1);
        }
    };
    
    template<class Allocator, class Fx, class Ret, class... Args>
    Call_base<Ret, Args...> * Make_call_fn(Allocator a, Fx&& fn) {
        using TypeEraseType = Call_fn<Allocator, Fx, Ret, Args...>;
        using ReboundAllocator = typename allocator_traits<Allocator>::
            template rebind_alloc<TypeEraseType>;
        ReboundAllocator aRebound(a);
        auto ptr = aRebound.allocate(1); // throws
        try {
            allocator_traits<ReboundAllocator>::construct(aRebound, ptr, a, forward<Fx>(fn));
        } catch (...) {
            aRebound.deallocate(ptr, 1);
            throw;
        }
    
        return ptr;
    }