【问题标题】:How are self-destructing type erasure classes like std::function implemented?像 std::function 这样的自毁类型擦除类是如何实现的?
【发布时间】:2016-06-27 09:46:57
【问题描述】:

我想了解std::function 的实现是如何工作的。为简单起见,让我们考虑不带参数的仅移动函数。

我了解std::function 通过典型的类型擦除技术擦除其目标的类型:

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_;
};

我想扩展这种类型的功能以支持自定义分配。我需要删除分配器的类型,但是使用std::unique_ptr 很难做到这一点。给unique_ptr 的自定义删除器需要知道给构造函数的Function 的具体类型,才能正确释放其存储空间。我可以使用另一个 unique_ptr 来键入擦除删除器,但该解决方案是循环的。

callable&lt;Function&gt; 似乎需要自行释放。这样做的正确方法是什么?如果我在callable&lt;Function&gt; 的析构函数中释放,那似乎还为时过早,因为它的成员还活着。

【问题讨论】:

  • 请注意,在 C++17 中将删除对 std::function 的分配器支持。

标签: c++ type-erasure self-destruction


【解决方案1】:

我认为不可能以可移植的方式做到这一点,依赖提供的分配器进行内存管理。

我一直在寻找std::shared_ptr 的实现,因为它支持type erasure also for it's deleter and allocator (see overload 6):在an sketch implementation I found around here 中使用了一个辅助对象来存储这些副本,但是这个对象是使用operator new 分配的并且是free'd使用operator delete,从而绕过提供的分配器和删除器。

我正在考虑使用分配器的临时副本(在堆栈上)以释放存储的分配器(从中制作副本)以及存储的对象。问题是:不知道类型,不使用new/delete,如何获取副本?不幸的是,covariance is ruled out 通过这个(需要返回一个指针)。

现在,我们来看看非标准解决方案:如果您喜欢使用 allocavariable length arrays,那么您可以使用删除器在堆栈上创建足够大的内存区域并让存储的分配器在该内存中创建自身的副本。这个堆栈分配的(因此自动存储持续时间)副本可以释放存储的分配器和存储的对象,并最终被删除器函数破坏(这一切的重点,不知道分配器的具体类型)。粗略的草图:

struct aux_base {
  // also provide access to stored function
  virtual size_t my_size(void) const = 0;
  virtual aux_base * copy_in(void * memory) const = 0;
  virtual void free(void * ptr) = 0;
  virtual ~aux_base() {}
};

template<class Alloc, class Function>
struct aux : public aux_base {
  // Store allocator and function here

  size_t my_size(void) const {
    return sizeof(*this);
  }
  aux_base * copy_in(void * memory) const {
    // attention for alignment issues!
    return new (memory) aux(*this);
  }
  void free(void * ptr) {
    aux * stored = reinterpret_cast<aux *>(ptr);
    // do your stuff
  }
};

void deleter_for_aux(aux_base * ptr) {
  char memory[ptr->my_size()];
  aux_base * copy = ptr->copy_in(memory);
  copy->free(ptr);
  copy->~aux_base(); // call destructor
}

也就是说,如果 有一种方法可以在标准 C++ 中做到这一点,而不依赖于提供的分配器之外的另一个动态内存源,我会很高兴知道它! :)

【讨论】:

  • > 不知道类型,不使用new/delete,如何获取副本?你有一个虚函数调用来为你做这件事。
  • @BillyONEal 您如何将该虚拟函数中的副本传递回其调用者?没有动态(堆)内存?
  • 什么副本? std::function 永远不需要分配器副本。
【解决方案2】:

std::function 在 C++17 中丢失了它的分配器,部分原因是类型擦除分配器的问题。但是,一般模式是将分配器重新绑定到您用于执行类型擦除的任何类型,将原始分配器存储在类型擦除的事物中,并在删除类型擦除的事物时再次重新绑定分配器。

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;
}

【讨论】:

    【解决方案3】:

    这是我想出的一个近似值。我不认为它完全正确,但它适用于我的用例。

    这个想法是使用带有unique_ptr 的“无操作”删除器。删除器调用对象的析构函数但不释放其存储空间。对象通过回调在其析构函数中自行释放。

    template<class Result>
    struct function
    {
      public:
        template<class Function>
        function(Function&& f)
          : f_(std::make_unique<callable_base>(std::forward<Function>(f)))
        {}
    
        template<class Alloc, class Function>
        function(const Alloc& alloc, Function&& f)
          : f_(allocate_unique(alloc, std::forward<Function>(f)))
        {}
    
        Result operator()() const
        {
          return (*f_)();
        }
    
      private:
        struct callable_base
        {
          // a deallocation callback to use within our destructor
          using deallocate_function_type = void(*)(callable_base*);
          deallocate_function_type deallocate_function;
    
          template<class Function>
          callable_base(Function callback)
            : deallocate_function(callback)
          {}
    
          virtual Result operator()() const = 0;
    
          virtual ~callable_base()
          {
            // deallocate this object's storage with the callback
            deallocate_function(this);
          }
        };
    
        template<class Alloc, class Function>
        struct callable : callable_base
        {
          mutable Function f;
    
          callable(Function&& f)
            : callable_base(deallocate),
              f(std::forward<Function>(f))
          {}
    
          virtual Result operator()() const
          {
            return f;
          }
    
          static void deallocate(callable_base* ptr)
          {
            // upcast to the right type of pointer
            callable* self = static_cast<callable*>(ptr);
    
            // XXX it seems like creating a new allocator here is cheating
            //     instead, we should use some member allocator, but it's
            //     not clear where to put it
            Alloc alloc;
            alloc.deallocate(self);
          }
        };
    
        struct self_deallocator_deleter
        {
          template<class T>
          void operator()(T* ptr) const
          {
            // call T's destructor but do not deallocate ptr
            ptr->~T();
          }
        };
    
        template<class Alloc, class Function>
        static std::unique_ptr<callable_base, self_deallocator_deleter>
          allocate_unique(const Alloc& alloc, Function&& f)
        {
          // allocate and construct the concrete callable object
          auto f_ptr = std::allocator_traits<Alloc>::allocate(alloc, 1);
          std::allocator_traits<Alloc>::construct(f_ptr, std::forward<Function>(f));
    
          // return the pointer through a unique_ptr
          return std::unique_ptr<callable_base,self_deallocator_deleter>(f_ptr);
        }
    
        std::unique_ptr<callable_base, self_deallocator_deleter> f_;
    };
    

    如果给定的分配器成为callable 对象的成员,而不是在callable::deallocate 内部动态创建新的分配器对象,则解决方案会更好。问题是我们不能让分配器成为callable 的成员,因为callable 对象在调用callable::deallocate 时不再存在。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-12-14
      • 2015-11-11
      • 1970-01-01
      • 2014-04-11
      • 2017-10-24
      • 1970-01-01
      • 2010-12-13
      • 1970-01-01
      相关资源
      最近更新 更多