如何在连续内存中以相同的继承层次结构多态存储和访问不同类型?

时间:2016-09-07 17:53:00

标签: c++ memory-management polymorphism c++14

对于多态性,通常的方法是使用std::vector<base*>。但是,我必须自己提供地址,即自己管理内存,无论是使用std::unique_ptr<>还是原始指针。

我希望polymorphic_storage<base>类型接受从base继承的任何类型。我还希望将类型存储在连续的内存中,以便更快地遍历和缓存相关的问题。

但是,存在一个非常大的问题:在存储级别缺少类型信息时,必须在调整大小时调用正确的移动/复制操作。

功能要求:

  • 可以将任何从基类继承的类型添加到存储中;没有固定的继承层次结构。
  • 继承类型必须在存储类型内正确对齐。
  • 必须调用正确的移动和复制操作,因为我没有处理POD类型。

我可以用什么机制来实现这个目标?

虽然我提供了答案,但我欢迎任何人发布他们的解决方案。

2 个答案:

答案 0 :(得分:10)

现在有对齐支持。

演示:http://coliru.stacked-crooked.com/a/c304d2b6a475d70c

这个答案侧重于解决问题中要求的三个功能。

  • 没有使用静态内存,因为如果将新类型添加到继承层次结构并且新类型超出静态限制,它将导致代码中断更改。
  • 存储空间内的所有类型都已正确对齐。
  • 重新分配时会调用正确的移动/复制构造函数。

它与fstrict-aliasing一起编译,所以不要太害怕reinterpret_cast<>()用法。

handle_base类型有一个名为void*的{​​{1}}数据成员,它指向某个值。它有两个成员函数,作用于src_

src_

使用placement-new移动或复制构造void transfer( void* dst, std::size_t& out_size ) src_指向的值,然后将dst设置为src_。它还将类型所占的大小(以字节为单位)添加到dst引用参数中;这对于正确对齐类型很有用。

out_size

返回指针void* src()

handle_base.h

src_

接下来,我创建了继承自namespace gut { template<class T> class handle; class handle_base { public: virtual ~handle_base() = default; handle_base() = default; handle_base( handle_base&& ) = default; handle_base( handle_base const& ) = default; handle_base& operator=( handle_base&& ) = default; handle_base& operator=( handle_base const& ) = default; void* src() const noexcept { return src_; } virtual void transfer( void* dst, std::size_t& out_size ) = 0; virtual void destroy() = 0; protected: handle_base( void* src ) noexcept : src_{ src } {} void* src_; }; } 的{​​{1}}类型,以便提供正确的移动/复制操作。此级别提供类型信息;这样可以实现从正确对齐到正确移动/复制操作的所有功能。

handle<T>

该函数将负责选择是使用移动还是复制构造函数。如果可用,将始终选择移动构造函数。它计算任何所需的对齐填充,将handle_base处的值传递给void transfer( void* dst, std::size_t& out_size ),并通过其大小和填充增加src_引用参数。

handle.h

dst + padding

由于我知道任何out_size namespace gut { template<class T> static std::size_t calculate_padding( void* p ) noexcept { std::size_t r{ reinterpret_cast<std::uintptr_t>( p ) % alignof( T ) }; return r == 0 ? 0 : alignof( T ) - r; } template <class T> class handle final : public handle_base { public: using byte = unsigned char; static_assert( sizeof( void* ) == sizeof( T* ), "incompatible pointer sizes" ); static constexpr std::integral_constant < bool, std::is_move_constructible<T>::value > is_moveable{}; handle( T* src ) noexcept : handle_base( src ) {} handle( handle&& ) = default; handle( handle const& ) = default; handle& operator=( handle&& ) = default; handle& operator=( handle const& ) = default; void transfer( std::true_type, void* dst ) noexcept( std::is_nothrow_move_assignable<T>::value ) { src_ = ::new ( dst ) T{ std::move( *reinterpret_cast<T*>( src_ ) ) }; } void transfer( std::false_type, void* dst ) noexcept( std::is_nothrow_copy_assignable<T>::value ) { src_ = ::new ( dst ) T{ *reinterpret_cast<T*>( src_ ) }; } virtual void transfer( void* dst, std::size_t& out_size ) noexcept( noexcept( std::declval<handle>().transfer( is_moveable, dst ) ) ) override { std::size_t padding{ gut::calculate_padding<T>( dst ) }; transfer( is_moveable, reinterpret_cast<byte*>( dst ) + padding ); out_size += sizeof( T ) + padding; } virtual void destroy() noexcept( std::is_nothrow_destructible<T>::value ) { reinterpret_cast<T*>( src_ )->~T(); src_ = nullptr; } }; } 的事实,我创建了一个sizeof( handle_base ) == sizeof( handle<T> )类型作为额外的间接,以方便使用。此类型可以包含任何T和重载polymorphic_handle,以便它可以充当任何句柄的通用句柄。

polymorphic_handle.h

handle<T>

现在所有构建块都存在,可以定义operator->()类型。它只存储namespace gut { class polymorphic_handle { public: using value_type = gut::handle_base; using pointer = value_type*; using const_pointer = value_type const*; template<class T> polymorphic_handle( gut::handle<T> h ) noexcept { ::new ( &h_ ) gut::handle<T>{ h }; } pointer operator->() { return reinterpret_cast<pointer>( &h_ ); } const_pointer operator->() const { return reinterpret_cast<const_pointer>( &h_ ); } private: std::aligned_storage_t<sizeof( value_type ), alignof( value_type )> h_; }; } ,缓冲区和大小信息。

存储类型确保只能添加​​从其模板参数类型派生的类。它只能使用初始实例或某些初始容量(以字节为单位)创建。

polymorphic_storage<T>

这个功能几乎可以完成所有工作。它确保有足够的容量用于指定为模板参数的类型,并在重新分配时将所有数据传输到新缓冲区。它还将std::vector<gut::polymorphic_handle>成员函数更新到下一个施工位置。

template<class D> void ensure_capacity()

这会将size_放入void emplace_back( D&& value ),并为新设置的值创建句柄。

value

以下是使用中的存储示例。请注意,polymorphic_storage<B>namespace gut { template<class B> class polymorphic_storage { public: using byte = unsigned char; using size_type = std::size_t; ~polymorphic_storage() noexcept { for ( auto& h : handles_ ) { h->destroy(); } std::free( data_ ); } explicit polymorphic_storage( size_type const initial_capacity ) { byte* new_data { reinterpret_cast<byte*>( std::malloc( initial_capacity ) ) }; if ( new_data ) { data_ = new_data; size_ = 0; capacity_ = initial_capacity; } else { throw std::bad_alloc{}; } } template < class D, std::enable_if_t<std::is_base_of<B, std::decay_t<D>>::value, int> = 0 > explicit polymorphic_storage( D&& value ) : data_{ nullptr } , size_{ 0 } , capacity_{ 0 } { using der_t = std::decay_t<D>; byte* new_data{ reinterpret_cast<byte*>( std::malloc( sizeof( der_t ) + alignof( der_t ) ) ) }; if ( new_data ) { data_ = new_data; size_ = sizeof( der_t ); capacity_ = sizeof( der_t ) + alignof( der_t ); handles_.emplace_back( gut::handle<der_t> { ::new ( data_ ) der_t{ std::forward<D>( value ) } } ); } else { throw std::bad_alloc{}; } } template < class D, std::enable_if_t<std::is_base_of<B, std::decay_t<D>>::value, int> = 0 > void emplace_back( D&& value ) { using der_t = std::decay_t<D>; ensure_capacity<der_t>(); der_t* p{ ::new ( data_ + size_ ) der_t{ std::forward<D>( value ) } }; size_ += sizeof( der_t ); handles_.emplace_back( gut::handle<der_t>{ p } ); } template < class D, std::enable_if_t<std::is_base_of<B, std::decay_t<D>>::value, int> = 0 > void ensure_capacity() { using der_t = std::decay_t<D>; auto padding = gut::calculate_padding<der_t>( data_ + size_ ); if ( capacity_ - size_ < sizeof( der_t ) + padding ) { auto new_capacity = ( sizeof( der_t ) + alignof( der_t ) + capacity_ ) * 2; auto new_data = reinterpret_cast<byte*>( std::malloc( new_capacity ) ); if ( new_data ) { size_ = 0; capacity_ = new_capacity; for ( auto& h : handles_ ) { h->transfer( new_data + size_, size_ ); } std::free( data_ ); data_ = new_data; } else { throw std::bad_alloc{}; } } else { size_ += padding; } } public: std::vector<gut::polymorphic_handle> handles_; byte* data_; size_type size_; size_type capacity_; }; } der0类型都来自der1,并且具有不同的大小和对齐方式。

演示:http://coliru.stacked-crooked.com/a/c304d2b6a475d70c

der2

答案 1 :(得分:5)

此方法试图避免在缓冲区中创建虚拟对象。相反,它创建了手动vtable,并扩展它们。

我们开始的第一件事是价值vtable,让我们虚拟地处理一个价值:

struct value_vtable {
  void(* copy_ctor)(void* dest, void const* src) = nullptr;
  void(* move_ctor)(void* dest, void* src) = nullptr;
  void(* dtor)(void* delete_this) = nullptr;
};

T类型创建其中一个,如下所示:

template<class T>
value_vtable make_value_vtable() {
  return {
    [](void* dest, void const* src) { // copy
      new(dest) T( *(T const*)src );
    },
    [](void* dest, void * src) { // move
      new(dest) T( std::move(*(T*)src) );
    },
    [](void* delete_this) { // dtor
      ((T*)delete_this)->~T();
    },
  };
}

我们可以将这些vtable内联存储,或者我们可以为它们创建静态存储:

template<class T>
value_vtable const* get_value_vtable() {
  static auto const table = make_value_vtable<T>();
  return &table;
}

此时我们还没有存储任何东西。

这是一个value_storage。它可以存储任何值(可以复制,移动和销毁):

template<std::size_t S, std::size_t A>
struct value_storage {
  value_vtable const* vtable;
  std::aligned_storage_t<S, A> data;
  template<class T,
    std::enable_if_t<!std::is_same< std::decay_t<T>, value_storage >{}, int> =0,
    std::enable_if_t< ( sizeof(T)<=S && alignof(T)<=A ), int > = 0
  >
  value_storage( T&& tin ) {
    new ((void*)&data) std::decay_t<T>( std::forward<T>(tin) );
    vtable = get_value_vtable<std::decay_t<T>>();
  }
  // to permit overriding the vtable:
protected:
  template<class T>
  value_storage( value_vtable const* vt, T&& t ):
    value_storage( std::forward<T>(t) )
  {
    vtable = vt;
  }
public:
  void move_from( value_storage&& rhs ) {
    clear();
    if (!rhs.vtable) return;
    rhs.vtable->move_ctor( &data, &rhs.data );
    vtable = rhs.vtable;
  }
  void copy_from( value_storage const& rhs ) {
    clear();
    if (!rhs.vtable) return;
    rhs.vtable->copy_ctor( &data, &rhs.data );
    vtable = rhs.vtable;
  }
  value_storage( value_storage const& rhs ) {
    copy_from(rhs);
  }
  value_storage( value_storage && rhs ) {
    move_from(std::move(rhs));
  }
  value_storage& operator=( value_storage const& rhs ) {
    copy_from(rhs);
    return *this;
  }
  value_storage& operator=( value_storage && rhs ) {
    move_from(std::move(rhs));
    return *this;
  }
  template<class T>
  T* get() { return (T*)&data; }
  template<class T>
  T const* get() const { return (T*)&data; }
  explicit operator bool() const { return vtable; }
  void clear() {
    if (!vtable) return;
    vtable->dtor( &data );
    vtable = nullptr;
  }
  value_storage() = default;
  ~value_storage() { clear(); }
};

此类型存储的内容类似于值,可能最大为S并且对齐A。它不存储它存储的类型,也就是别人的工作。它确实存储了如何复制,移动和销毁它存储的任何内容,但它不知道它存储的是什么。

它假设在块中构造的对象构造在它的前面。如果您不想做出这样的假设,可以添加void* ptr字段。

现在我们可以通过操作扩充此value_storage

特别是,我们希望演员阵容。

template<class Base>
struct based_value_vtable:value_vtable {
  Base*(* to_base)(void* data) = nullptr;
};

template<class Base, class T>
based_value_vtable<Base> make_based_value_vtable() {
  based_value_vtable<Base> r;
  (value_vtable&)(r) = make_value_vtable<T>();
  r.to_base = [](void* data)->Base* {
    return (T*)data;
  };
  return r;
}

template<class Base, class T>
based_value_vtable<Base> const* get_based_value_vtable() {
  static const auto vtable = make_based_value_vtable<Base, T>();
  return &vtable;
}

现在我们已将value_vtable扩展为包含“vtable with base”系列。

template<class Base, std::size_t S, std::size_t A>
struct based_value:value_storage<S, A> {
  template<class T,
    std::enable_if_t< !std::is_same< std::decay_t<T>, based_value >{}, int> = 0
  >
  based_value( T&& tin ):
    value_storage<S, A>(
      get_based_value_vtable<Base, std::decay_t<T> >(),
      std::forward<T>(tin)
    )
  {}

  template<class T>
  based_value(
    based_value_vtable<Base> const* vt,
    T&& tin
  ) : value_storage<S, A>( vt, std::forward<T>(tin) )
  {}

  based_value() = default;
  based_value( based_value const& ) = default;
  based_value( based_value && ) = default;
  based_value& operator=( based_value const& ) = default;
  based_value& operator=( based_value && ) = default;
  based_value_vtable<Base> const* get_vt() const {
    return static_cast< based_value_vtable<Base>* >(this->vtable);
  }
  Base* get() {
    if (!*this) return nullptr;
    return get_vt()->to_base( &this->data );
  }
  Base const* get() const {
    if (!*this) return nullptr;
    return get_vt()->to_base( (void*)&this->data );
  }
};

这些是本地存储的常规值类型,具有多态性,可以是Base的任何后代,符合大小和对齐要求。

只需存储这些矢量。这解决了你的问题。这些对象满足std::vector期望的值类型的公理。

live example。代码没有经过严格测试(你可以看到非常小的测试),它可能仍然包含一些拼写错误。但设计很扎实,我以前做过这个。

使用operator*operator->进行扩充是一个留给读者的练习。如果你想以某种大小的代价获得极高的性能,你可以将vtable函数指针内联存储在类中,而不是存储在共享内存中。

如果您发现自己不止一次这样做或向based_value添加新功能,那么比定制based_value技巧更好的扩展就是自动化vtable扩展程序。我会使用type erasure with std::any之类的内容,只需将any替换为value_storage<Size, Align>以保证自动存储,添加更好的const支持,并将两个vtable集成为一个(比如上面的based_value

最后,我们得到:

template<class T>
auto to_base = [](auto&& self)->copy_const< decltype(self), T& > {
  return decltype(self)(self);
};

template<class Base, std::size_t S, std::size_t A>
using based_value = super_value_storage< S, A, decltype(to_base<Base>) >;

using my_type = based_value< Some_Base, 100, 32 >;

my_type bob = // some expression
if (bob)
  return (bob->*to_base<Some_Base>)()
else
  return nullptr;

或某些。

所有C风格的演员阵容都可以用静态和const演员组合替换,但我很懒。我不相信我做过任何需要重新诠释的事情。

但实际上,一旦你拥有了这种基于魔法价值的多态性,为什么还要打扰基地呢?只需擦除值所需的所有操作,并接受任何支持擦除操作的操作。

any_method中仔细使用ADL后,您现在可以将不同的对象概念映射到必须支持的概念集。如果你知道如何给狗矢量鸡,你可以直接在super_value_storage< Size, Align, ..., decltype(do_the_chicken) >中存储一个狗的载体。

但是,这可能会走得太远。