Ответ 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();
}