对于多态性,通常的方法是使用std::vector<base*>
。但是,我必须自己提供地址,即自己管理内存,无论是使用std::unique_ptr<>
还是原始指针。
我希望polymorphic_storage<base>
类型接受从base
继承的任何类型。我还希望将类型存储在连续的内存中,以便更快地遍历和缓存相关的问题。
但是,存在一个非常大的问题:在存储级别缺少类型信息时,必须在调整大小时调用正确的移动/复制操作。
功能要求:
我可以用什么机制来实现这个目标?
虽然我提供了答案,但我欢迎任何人发布他们的解决方案。
答案 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) >
中存储一个狗的载体。