如何设计符合标准的 std::any 实现的存储?

Posted

技术标签:

【中文标题】如何设计符合标准的 std::any 实现的存储?【英文标题】:How can I design storage that conforms to the standard's implementation of std::any? 【发布时间】:2016-05-07 23:30:44 【问题描述】:

标准工作草案 (n4582, 20.6.3, p.552) 对std::any 的实现提出以下建议:

实现应避免为包含的小对象使用动态分配的内存。 [ 示例:构造的对象仅包含一个 int。 —end example ] 这种小对象优化仅适用于 is_nothrow_move_constructible_v 为 true 的类型 T。

据我所知,std::any 可以通过类型擦除/虚函数和动态分配内存轻松实现。

如果在销毁时不知道编译时间信息,std::any 如何避免动态分配并仍然销毁这些值;如何设计符合标准建议的解决方案?


如果有人想查看非动态部分的可能实现,我在 Code Review 上发布了一个:https://codereview.stackexchange.com/questions/128011/an-implementation-of-a-static-any-type

这里的答案有点太长了。它基于 Kerrek SB 对以下 cmets 的建议。

【问题讨论】:

也许使用union @KerrekSB 我理解小对象优化背后的前提(例如 std::string 的 SSO)。但是,我看不到在不知道类型的情况下如何调用析构函数。 @user2296177:私有的、类型擦除的派生类类似于Impl<T>;如果您知道sizeof(Impl<T>) 不超过sizeof(any)(可能加上一个额外的词左右),那么您可以在对象本身中就地构造Impl<T>。您无需动态分配内存即可动态创建对象。 设置包含值的时候就知道类型了。将析构函数保存在 std::function 成员变量中,当 any 被析构时调用该成员变量。 @user2296177:析构函数是类型擦除的一部分,对吧?您只需拨打ptr()->~Base() 【参考方案1】:

通常,any 接受任何东西并从中动态分配一个新对象:

struct any 
    placeholder* place;

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

    ~any() 
        delete place;
    
;

我们使用placeholder 是多态的这一事实来处理我们所有的操作——销毁、强制转换等。但是现在我们想要避免分配,这意味着我们避免了多态给我们带来的所有好处——并且需要重新实现它们。首先,我们将有一些联合:

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

我们有一些template &lt;class T&gt; is_small_object ... 来决定我们是在做ptr = new holder&lt;T&gt;(value) 还是new (&amp;buffer) T(value)。但是构造并不是我们唯一要做的事情——我们还必须进行破坏和类型信息检索,这取决于我们所处的情况而有所不同。要么我们正在做delete ptr,要么我们正在做@ 987654329@,后者依赖于跟踪T

所以我们引入了我们自己的类似 vtable 的东西。然后我们的any 会坚持:

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

您可以改为为每个操作创建一个新的函数指针,但我可能在这里缺少其他几个操作(例如OP_CLONE,这可能需要将传入的参数更改为union ...) 而且你不想用一堆函数指针来膨胀你的any 大小。通过这种方式,我们损失了一点点性能,以换取尺寸上的巨大差异。

在构建时,我们会同时填充storagevtable

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))
 

我们的VTable 类型类似于:

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

然后我们只需使用该 vtable 来实现我们的各种操作。喜欢:

~any() 
    vtable(OP_DESTROY, storage, nullptr);

【讨论】:

【参考方案2】:

std::any 如何避免动态分配并仍然销毁此类 值,如果在时间不知道编译时间信息 破坏

这似乎是一个加载的问题。最新的draft 需要这个构造函数:

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

我想不出为什么需要“类型擦除”,除非您希望代码同时处理小型 大型情况。但是为什么没有这样的东西呢?1

template <typename T>
  struct IsSmallObject : ...type_traits...
小对象:使用placement new 和显式析构函数调用。 大对象:使用allocator

在前一种情况下,您可以有一个指向未初始化存储的指针:

union storage

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

建议使用联合作为@KerrekSB。

请注意,存储类不需要知道类型。使用某种句柄/调度(不确定成语的真实名称)系统在这一点上变得微不足道。

首先让我们来看看破坏是什么样子的:

  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;
    

    // ...
   ;

然后是any 类:

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

  // ...

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

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

这里我们将需要知道编译时类型的逻辑分解到处理程序/调度系统中,而 any 类的大部分只需要处理 RTTI。


1:这是我要检查的条件:

    nothrow_move_constructible sizeof(T) &lt;= sizeof(storage)。就我而言,这是3 * sizeof(void*) alignof(T) &lt;= alignof(storage)。就我而言,这是std::alignment_of&lt;void*&gt;::value

【讨论】:

【参考方案3】:

灵感来自boost any 我想出了这个(test it on ideone)(我创建了一个最小的案例来展示如何在没有动态内存的情况下销毁像any 这样的类型擦除容器。我只关注构造函数/析构函数,省略了其他所有内容,忽略移动语义和其他东西)

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

输出:

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

当然第一个是被销毁的临时对象,最后两个证明Any的销毁调用了正确的析构函数。

诀窍是拥有多态性。这就是我们有Base_holderHolder 的原因。我们通过 std::aligned_storage 中的新位置初始化它们,并显式调用析构函数。

这只是为了证明你可以在不知道Any 持有的类型的情况下调用正确的析构函数。当然,在实际的实现中,你会有一个联合,或者一个指向动态分配内存的指针和一个布尔值,告诉你你有哪一个。

【讨论】:

@Barry 当然。这就是为什么你实际上会有一个工会。为此以及动态分配的内存。 严格来说static_cast&lt;Base_holder*&gt;(static_cast&lt;void*&gt;(&amp;buffer_))是UB。 “指向结构的指针是指向其第一个子对象的指针”仅适用于标准布局类型,Holder&lt;T&gt; 不是。 @BenVoigt 据我所知,演员表与子对象无关。我使用buffer_ 作为Holder 的存储空间。我用placement new 构造它,然后通过基类Base_holder 调用析构函数,并使用指针来实现多态性。我对标准不是很熟悉,但我觉得还可以。 @BenVoigt 我应该就这个具体问题发表一个问题吗? @bolov:你使用placement new来构造一个Holder&lt;T&gt;,它应该在buffer_的开头对齐。但是稍后您创建了一个指向Base_holder 的指针,而没有执行适当的向上转换。不能保证Base_holder 子对象从Holder&lt;T&gt; 对象的第一个字节开始(标准明确定义了何时做出这样的保证——对于具有标准布局类型的对象——并且Holder&lt;T&gt; 不是标准的——布局)。

以上是关于如何设计符合标准的 std::any 实现的存储?的主要内容,如果未能解决你的问题,请参考以下文章

带有 std::any 和 std::optional 的 any_cast

是否可以仅从 std::any 使用 std::reference_wrapper 创建 std::any?

获取 std::any 的大小

如何打印使用“std::any”类型的变量插入的字符串向量的元素

std::any 由 std::exception_ptr

以下哪个设计符合表设计规范