7

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

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

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

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


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

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

4

3 回答 3

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 <class T> is_small_object { ... }来决定我们是否正在做ptr = new holder<T>(value)new (&buffer) T(value)。但是构造并不是我们唯一要做的事情——我们还必须进行破坏和类型信息检索,这取决于我们所处的情况而有所不同。要么我们正在做,要么delete ptr我们正在做static_cast<T*>(&buffer)->~T();,后者这取决于跟踪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);
}
于 2016-05-08T14:53:31.417 回答
0

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

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

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

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

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

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

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:这是我要检查的条件:

  1. nothrow_move_constructible
  2. sizeof(T) <= sizeof(storage). 就我而言,这是3 * sizeof(void*)
  3. alignof(T) <= alignof(storage). 就我而言,这是std::alignment_of<void*>::value
于 2016-05-08T01:09:01.100 回答
-1

受到boost any我的启发(在 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。我们通过在 a 中放置 new 来初始化它们,std::aligned_storage并显式调用析构函数。

这只是为了证明您可以在不知道Any. 当然,在真正的实现中,你会有一个联合,或者一个指向动态分配内存的指针和一个布尔值,告诉你你有哪一个。

于 2016-05-08T00:58:16.087 回答