Как я могу полиморфно хранить и получать доступ к различным типам из одной и той же иерархии наследования в непрерывной памяти?

Для полиморфизма обычный подход заключается в использовании std::vector<base*>. Однако я должен сам предоставлять адреса, то есть управлять самой памятью, использую ли я std::unique_ptr<> или необработанные указатели.

Я хотел бы иметь тип polymorphic_storage<base>, который принимает любой тип, который наследуется от base. Я также хочу, чтобы типы сохранялись в непрерывной памяти для более быстрого обхода и проблем, связанных с кешем.

Однако существует довольно большая проблема: при отсутствии информации о типе на уровне хранилища правильные операции перемещения/копирования должны вызываться при изменении размера.

Запрос функции:

  • Любой тип, который наследуется от базового класса, может быть добавлен в хранилище; нет фиксированных иерархий наследования.
  • Наследуемые типы должны быть правильно выровнены внутри типа хранилища.
  • Правильные операции перемещения и копирования должны быть вызваны, поскольку я не имею дело с типами POD.

Какой механизм я могу использовать для достижения этого?


Пока я даю ответ, я приветствовал бы всех, кто разместил их решение.

Ответы

Ответ 1

Теперь с поддержкой выравнивания.

Демо: http://coliru.stacked-crooked.com/a/c304d2b6a475d70c


Этот ответ посвящен решению трех функций, заданных в вопросе.

  • Не используется статическая память, потому что это приведет к изменению кода, если новый тип добавлен в иерархию наследования и что новый тип превышает статический предел.
  • Все типы внутри хранилища правильно выровнены.
  • Правильные конструкторы move/copy вызываются при перераспределении.

Он компилируется с помощью fstrict-aliasing, поэтому не бойтесь этого использования reinterpret_cast<>().


Тип handle_base имеет элемент данных void*, называемый src_, который указывает на некоторое значение. Он имеет две функции-члены, которые действуют на src_.

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

Использует новое размещение для перемещения или копирования, создавая значение, на которое указывает src_ в dst, затем устанавливает src_ в dst. Он также добавляет размер в байтах, сделанный типом в ссылочный аргумент out_size; это полезно для правильного выравнивания типов.

void* src()

Возвращает указатель src_.

handle_base.h

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>, который наследует от handle_base, чтобы обеспечить правильные операции перемещения/копирования. Информация о типе доступна на этом уровне; это позволяет все от правильного выравнивания до правильных операций перемещения/копирования.

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

Функция позаботится о том, следует ли использовать конструктор перемещения или копирования. Конструктор перемещения всегда будет выбран, если он доступен. Он вычисляет любое требуемое дополнение для выравнивания, передает значение в src_ в dst + padding и увеличивает опорный аргумент out_size по его размеру и дополнению.

handle.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 в качестве дополнительной косвенности для простоты использования. Этот тип может содержать любые handle<T> и перегружает operator->(), чтобы он мог действовать как общий дескриптор для любого дескриптора.

polymorphic_handle.h

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

Этот подход пытался избежать создания виртуальных объектов в буфере. Вместо этого он создает вручную vtables и расширяет их.

Первое, с чего мы начнем, - это значение 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;
}

В этот момент мы ничего не сохранили.

Вот значение_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 с базой".

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.

живой пример. Код не сильно тестирован (вы можете увидеть очень маленький тест), он, вероятно, все еще содержит некоторые опечатки. Но дизайн прочный, я сделал это раньше.

Увеличение с помощью operator* и operator-> - это упражнение, оставленное читателю. Если вам нужна экстремальная производительность за счет некоторого размера, вы можете сохранить указатели на функцию vtable внутри класса, а не в общей памяти.


Если вы обнаружите, что делаете это более одного раза или добавляете новые возможности в свой based_value, лучшее расширение, чем сделанный на заказ трюк based_value, должно было бы автоматизировать процедуру расширения vtable. Я использовал бы что-то вроде типа erasure с std::any, просто заменив any на value_storage<Size, Align> для гарантированного автоматического хранения, добавив лучшую const поддержку, и интегрируя два vtables в один (как в 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;

или somesuch.

Все стили C-стиля могут быть заменены комбинацией статических и const-ролей, но я стал ленивым. Я не верю, что когда-либо делаю что-то, требующее повторного толкования.

Но действительно, как только у вас есть такой магии, основанный на значении полиморфизма, зачем вообще заниматься базой? Просто удалите все операции, которые вы хотите по значению, и примите все, что поддерживает стертые операции.

При внимательном использовании ADL в any_method s, вы можете теперь концептуально отобразить различные объекты на свой набор концепций, которые необходимо поддерживать. Если вы знаете, как курить вектор собак, вы можете прямо сохранить вектор собак в super_value_storage< Size, Align, ..., decltype(do_the_chicken) >.

Однако это может зайти слишком далеко.