代码之家  ›  专栏  ›  技术社区  ›  user2296177

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

  •  14
  • user2296177  · 技术社区  · 10 年前

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

    我想要一个 polymorphic_storage<base> 接受从继承的任何类型的类型 base

    然而,有一个很大的问题:在存储级别缺少类型信息的情况下,必须在调整大小时调用正确的移动/复制操作。

    功能请求:

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


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

    2 回复  |  直到 10 年前
        1
  •  10
  •   Community Mohan Dere    6 年前

    现在有了对齐支持。

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


    这个答案着重于解决问题中要求的三个特征。

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

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


    这个 handle_base 类型具有 void* 已调用数据成员 src_ ,它指向一些值。它有两个作用于 源代码_ .

    void transfer( void* dst, std::size_t& out_size )

    源代码_ dst ,然后设置 源代码_ 分布式存储系统 。它还将类型所占的大小(以字节为单位)添加到 out_size 参考参数;这对于正确对齐类型很有用。

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

    接下来,我创建 handle<T> 把手_底座 以便提供正确的移动/复制操作。类型信息在此级别可用;这可以实现从正确对齐到正确移动/复制操作的所有操作。

    void传输(void*dst,std::size_t&out_size)

    该函数将负责选择是使用移动构造函数还是复制构造函数。如果可用,将始终选择移动构造函数。它计算对齐所需的任何填充,在 源代码_ dst + padding 并增加 引用参数的大小和填充。

    手柄.h

    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 手柄<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 der2 继承自 base 和具有不同的大小和对齐方式。

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

    #include <iostream>
    #include <string>
    
    struct base
    {
        virtual ~base() = default;
        virtual void print() const = 0;
    };
    
    struct der0 : public base
    {
        der0( int&& i ) noexcept : i_{ i } {}
        void print() const override { std::cout << "der0_" << i_ << '\n'; }
        int i_;
    };
    
    struct der1 : public base
    {
        der1( std::string const& s ) noexcept : s_{ s } {}
        void print() const override { std::cout << "der1_" << s_ << '\n'; }
        std::string s_;
    };
    
    struct der2 : public base
    {
        der2( std::string&& s ) noexcept : s_{ std::move( s ) } {}
        void print() const override { std::cout << "der2_" << s_ << '\n'; }
        std::string s_;
        double d[ 22 ];
    };
    
    int main()
    {
        gut::polymorphic_storage<base> ps{ 32 };
        ps.emplace_back( der1{ "aa" } );
        ps.emplace_back( der2{ "bb" } );
        ps.emplace_back( der1{ "cc" } );
        ps.emplace_back( der2{ "ee" } );
        ps.emplace_back( der0{ 13 } );
        ps.emplace_back( der2{ "ff" } );
    
        for ( auto handle : ps.handles_ )
            reinterpret_cast<base*>( handle->src() )->print();
    }
    
        2
  •  5
  •   Jonathan Leffler    8 年前

    这种方法试图避免在缓冲区中创建虚拟对象。相反,它创建并扩展手动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();
        },
      };
    }
    

    我们可以在线存储这些vtables,也可以为它们创建静态存储:

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


    如果您发现自己不止一次这样做,或者在您的 based_value ,比定制的更好的扩展 基本值 技巧是自动化vtable扩展过程。我会用类似这样的东西 type erasure with std::any ,只需替换 any 使用 value_storage<Size, Align> 保证自动存储,添加更好 const 支持,并将两个vtable集成为一个(如 基本值

    最后,我们会得到:

    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样式的强制转换都可以用静态和常量强制转换的组合来替换,但我很懒。我认为我从来没有做过任何需要重新解释演员阵容的事情。

    在您的 any_method s、 现在,您可以将不同的对象映射到您必须支持的概念集。如果你知道如何对一个向量的狗进行鸡,你可以直接在 super_value_storage< Size, Align, ..., decltype(do_the_chicken) > .

    然而,这可能太过分了。