如何解决用未定义的行为代替没有指针的多态性
我最近在 this question 上询问过这个问题。我已经采用了这种通用方法:
#define COFFEE_GEN_GENERIC_VALUE(CLASS_TYPE) using GenericValue##CLASS_TYPE = coffee::utils::GenericValue<CLASS_TYPE,COFFEE_GENERIC_VALUE_CONfig_MAX_DERIVED_SIZE_OF_##CLASS_TYPE>
template<typename Base,size_t MaxDerivedSize>
class GenericValue
{
private:
char buffer[MaxDerivedSize];
public:
GenericValue() = default;
template<typename Derived,typename... ConstructorArgs>
static GenericValue<Base,MaxDerivedSize> create(ConstructorArgs... args)
{
static_assert(std::is_base_of<Base,Derived>::value,"Derived must derive from Base.");
static_assert(sizeof(Derived) <= MaxDerivedSize,"The size specified with the macro COFFEE_GenericValueMaxSize is to small.");
GenericValue<Base,MaxDerivedSize> result{};
new ((Derived*) result.buffer) Derived{args...};
return result;
}
template<typename Derived>
static GenericValue<Base,MaxDerivedSize> from(Derived* tocopy)
{
static_assert(std::is_base_of<Base,"Derived must derive from Base.");
static_assert(sizeof(Derived) <= sizeof(MaxDerivedSize),MaxDerivedSize> result{};
memcopy(result.buffer,tocopy,sizeof(Derived));
return result;
}
GenericValue(const GenericValue<Base,MaxDerivedSize>& other)
{
memcopy(buffer,other.buffer,MaxDerivedSize);
}
GenericValue(GenericValue<Base,MaxDerivedSize>&& other)
{
memcopy(buffer,MaxDerivedSize);
}
GenericValue<Base,MaxDerivedSize>& operator=(const GenericValue<Base,MaxDerivedSize);
return *this;
}
GenericValue<Base,MaxDerivedSize>& operator=(GenericValue<Base,MaxDerivedSize>&& other)
{
coffee::utils::copy(buffer,MaxDerivedSize);
return *this;
}
Base& operator*()
{
return *(Base*)buffer;
}
Base* operator->()
{
return (Base*)buffer;
}
}
我对这种形式 this website 有想法,它与@Matthias Grün 在我的旧问题上的 awnser 类似。
您使用 COFFEE_GEN_GENERIC_VALUE
宏为 GenericValue<Base,MaxDerivedSize>
创建一个 typedef,大小将通过第二个宏传递给它。这样我的库就可以生成将用于特定基类的适当 GenericValue
类型。它背后的基本思想是将派生类型的整个实例及其 vtable 存储在 buffer
中。您可以像复制常规右值一样复制它。
不幸的是,复制只在某些时候有效,我不知道为什么,根据我的知识,这不会成为问题。如果我删除复制运算符和构造函数,并仅使用该类作为 new
的包装器,它会很好地工作(除非对象并未真正复制)。
有没有人知道可能是什么问题?
当我发现一个不需要发布整个项目的边缘案例时,我会发布一个具体示例。
解决方法
memcpy
不是“纯旧数据”的数据是未定义的行为。当你在做恶作剧时,避免未定义的行为。
所以你需要做的第一件事是在你的缓冲区旁边存储一个指向复制操作的指针。然后,当您复制缓冲区时,而不是 memcpy,您调用该指针复制操作。
一个简单的方法是使用 void(*)(void const* src,void* dst)
函数指针从 src
复制到 dst
。
将内存强制转换为它不是的类型并使用该指针是未定义的行为。
所以你需要做的第二件事是存储一种从内存缓冲区中获取指向基址的指针的方法。一个简单的方法是存储一个 Base*(*)(void*)
函数指针,并将其传递给缓冲区。
对于一些间接的成本,您可以将每个实例的开销减少到这样的单个指针。
struct BasicVTable {
void(* destroy)(void*) = 0;
template<class T>
static BasicVTable make() {
return {[](void* ptr){ static_cast<T*>(ptr)->~T(); }};
}
template<class T>
static auto const* get() {
static const auto vtable = make<T>();
return &vtable;
}
};
struct RegularVTable : BasicVTable {
void(*assign)(void const* src,void* dst) = 0;
void(*assign_move)(void* src,void* dst) = 0;
void(*copy)(void const* src,void* dst) = 0;
void(*move)(void* src,void* dst) = 0;
template<class T>
static RegularVTable make() {
return {
BasicVTable::template make<T>(),[](void const* src,void* dst) {
*static_cast<T*>(dst) = *static_cast<T const*>(src);
},[](void* src,void* dst) {
*static_cast<T*>(dst) = std::move(*static_cast<T*>(src));
},void* dst) {
::new(dst) T(*static_cast<T const*>(src));
},void* dst) {
::new(dst) T(std::move(*static_cast<T*>(src)));
}
};
}
template<class T>
static auto const* get() {
static const auto vtable = make<T>();
return &vtable;
}
};
template<class Base>
struct PolyBaseVTable : RegularVTable {
Base*(*GetBase)(void* src) = 0;
Base const*(*cGetBase)(void const* src) = 0;
template<class T>
static PolyBaseVTable make() {
return {
RegularVTable::make<T>(),[](void* src)->Base* {
return static_cast<T*>(src);
},[](void const* src)->Base const* {
return static_cast<T const*>(src);
}
};
}
template<class T>
static auto const* get() {
static const auto vtable = make<T>();
return &vtable;
}
};
现在我们修改您的GenericValue
:
template<typename Base,size_t MaxDerivedSize>
class GenericValue
{
private:
PolyBaseVTable<Base> const* vtable = nullptr;
char buffer[MaxDerivedSize];
存储一个额外的指针。
我们做了一些细微的修改:
template<typename Derived,typename... ConstructorArgs>
static GenericValue<Base,MaxDerivedSize> create(ConstructorArgs... args)
{
static_assert(std::is_base_of<Base,Derived>::value,"Derived must derive from Base.");
static_assert(sizeof(Derived) <= MaxDerivedSize,"The size specified with the macro COFFEE_GenericValueMaxSize is to small.");
GenericValue<Base,MaxDerivedSize> result{};
::new ((void*) result.buffer) Derived{args...}; // void* here
result.vtable = PolyBaseVTable<Base>::template get<Derived>();
return result;
}
我们在构造对象后初始化 vtable
的地方。
你的from
是胡说八道。这只是另一个create
。
然后复制构造是:
GenericValue(const GenericValue<Base,MaxDerivedSize>& other)
{
if (!other.vtable) return;
other.vtable->copy( &other.buffer,&buffer );
vtable = other.vtable;
}
其他分配/移动操作的工作方式类似。分配有点痛苦:
GenericValue& operator=(const GenericValue& other)
{
if (vtable != other.vtable)
{
if (vtable) vtable->destroy(&buffer);
vtable = nullptr;
if (other.vtable)
{
other.vtable->copy(&other.buffer,&buffer);
vtable=other.vtable;
return *this;
}
}
if (!vtable) return *this;
vtable->assign( &other.buffer,&buffer );
return *this;
}
因为您必须处理类型更改的可能性。
对于基本访问:
Base* operator->()
{
if (!vtable) return nullptr;
return vtable->GetBase( &buffer );
}
Base const* operator->() const
{
if (!vtable) return nullptr;
return vtable->cGetBase( &buffer );
}
简单易行。
现在,这段代码确实使用了 c++11 之后的扩展聚合规则,因此如果您是仅限于 c++11。但没有什么真正棘手的。
此外,自动返回类型推导(我在一些地方使用了 static make
)可能必须替换为实际类型。预期的类型并不难解决,我只是没有,因为我不想在那里重复自己。
当然,您还需要在 auto
上调用 vtable->destroy
。
现在,您可能会注意到我将操作表设为 ~GenericValue
。这是因为我正在以 C-with-enhanced-C++-code-generation 方式重新实现一个简单版本的基于经典 C++ 的多态性。
您实际上可以使用它来实现强制转换为基数以外的操作,以避免额外的 vtable 命中。我只会在检测到额外 vtable 命中的性能的情况下考虑这一点,或者当我想要存储异质对象(例如 vtable
的工作方式)并支持它们的多态性时。
Live example 正在运行中。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。