支持多态的Stored-by-Value Pool,如何使用智能指针?
Stored-by-Value Pool that support polymorphism, how to use smart pointer?
简介
我有一个数据结构:值池。 (不是指针池)
当我调用 create() 时,它将 return Handle。
到目前为止一切都很好。
template<class T> class Pool{
std::vector<T> v; //store by value
Handle<T> create(){ .... }
}
template<class T> class Handle{
Pool<T>* pool_; //pointer back to container
int pool_index_; //where I am in the container
T* operator->() {
return pool_->v.at(pool_index_); //i.e. "pool[index]"
}
void destroy(){
pool_-> ... destroy(this) .... mark "pool_index_" as unused, etc ....
}
}
现在我希望 Handle<> 支持多态性。
问题
很多高手都好心建议我用weak_ptr,可是我还是空白了一个星期,不知道怎么做。
我卡住的主要部分是:-
应该create()returnweak_ptr,而不是句柄?
....或者应该 Handle 封装 weak_ptr?
如果 create() return weak_ptr 对于用户程序,.. .
weak_ptr 怎么知道 pool_index_?它没有这样的字段。
如果用户将 weak_ptr/Handle 转换为父 class 指针如下,还有很多问题:-
例如
class B{}
class C : public B { ......
}
....
{
Pool<C> cs;
Handle<C> cPtr=cs.create();
Handle<B> bPtr=cPtr; // casting ;expected to be valid,
// ... but how? (weak_ptr may solve it)
bPtr->destroy() ; // aPtr will invoke Pool<B>::destroy which is wrong!
// Pool<C>::destroy is the correct one
bPtr.operator->() ; // face the same problem as above
}
假设
- Pool 总是在 Handle 之后删除(为简单起见)。
- 没有多线程
这里有类似的问题,但 none 足够接近了。
C++11 memory pool design pattern?
关于weak_ptr
A std::weak_ptr
总是与 std::shared_ptr
关联。要使用 weak_ptr
,您必须使用 shared_ptr
来管理您的对象。这意味着您的对象的所有权可以共享:任何人都可以从 weak_ptr
构造一个 shared_ptr
并将其存储在某个地方。只有当所有 shared_ptr
都被销毁时,指向的对象才会被删除。 Pool
将失去对对象释放的直接控制,因此无法支持 public destroy()
函数。
使用共享所有权,事情会变得非常混乱。
这就是为什么 std::unique_ptr
通常是对象生命周期管理的更好选择的原因之一(遗憾的是它不适用于 weak_ptr
)。您的 Handle::destroy()
函数还暗示这不是您想要的,并且 Pool
应该单独处理其对象的生命周期。
但是,shared_ptr
/weak_ptr
是为多线程应用程序设计的。在单线程环境中,您可以获得类似 weak_ptr
的功能(检查有效目标并避免悬挂指针),而无需使用 weak_ptr
:
template<class T> class Pool {
bool isAlive(int index) const { ... }
}
template<class T> class Handle {
explicit operator bool() const { return pool_->isAlive(pool_index_); }
}
为什么这只适用于单线程环境?
在多线程程序中考虑这个场景:
void doSomething(std::weak_ptr<Obj> weak) {
std::shared_ptr<Obj> shared = weak.lock();
if(shared) {
// Another thread might destroy the object right here
// if we didn't have our own shared_ptr<Obj>
shared->doIt(); // And this would crash
}
}
在上面的例子中,我们必须确保指向的对象在if()
之后仍然可以访问。因此,我们构建了一个 shared_ptr
来让它保持活力——无论如何。
在单线程程序中你不必担心:
void doSomething(Handle<Obj> handle) {
if(handle) {
// No other threads can interfere
handle->doIt();
}
}
多次取消引用句柄时仍然要小心。示例:
void doDamage(Handle<GameUnit> source, Handle<GameUnit> target) {
if(source && target) {
source->invokeAction(target);
// What if 'target' reflects some damage back and kills 'source'?
source->payMana(); // Segfault
}
}
但使用另一个 if(source)
,您现在可以轻松检查句柄是否仍然有效!
铸造手柄
因此,Handle<T>
中的模板参数 T
不一定与池的类型匹配。也许你可以用模板魔法解决这个问题。我只能想出一个使用动态调度(虚拟方法调用)的解决方案:
struct PoolBase {
virtual void destroy(int index) = 0;
virtual void* get(int index) = 0;
virtual bool isAlive(int index) const = 0;
};
template<class T> struct Pool : public PoolBase {
Handle<T> create() { return Handle<T>(this, nextIndex); }
void destroy(int index) override { ... }
void* get(int index) override { ... }
bool isAlive(int index) const override { ... }
};
template<class T> struct Handle {
PoolBase* pool_;
int pool_index_;
Handle(PoolBase* pool, int index) : pool_(pool), pool_index_(index) {}
// Conversion Constructor
template<class D> Handle(const Handle<D>& orig) {
T* Cannot_cast_Handle = (D*)nullptr;
(void)Cannot_cast_Handle;
pool_ = orig.pool_;
pool_index_ = orig.pool_index_;
}
explicit operator bool() const { return pool_->isAlive(pool_index_); }
T* operator->() { return static_cast<T*>( pool_->get(pool_index_) ); }
void destroy() { pool_->destroy(pool_index_); }
};
用法:
Pool<Impl> pool;
Handle<Impl> impl = pool.create();
// Conversions
Handle<Base> base = impl; // Works
Handle<Impl> impl2 = base; // Compile error - which is expected
检查有效转换的行可能会被优化掉。检查仍将在编译时进行!尝试无效转换会给您这样的错误:
error: invalid conversion from 'Base*' to 'Impl*' [-fpermissive]
T* Cannot_cast_Handle = (D*)nullptr;
我在这里上传了一个简单的、可编译的测试用例:http://ideone.com/xeEdj5
简介
我有一个数据结构:值池。 (不是指针池)
当我调用 create() 时,它将 return Handle。
到目前为止一切都很好。
template<class T> class Pool{
std::vector<T> v; //store by value
Handle<T> create(){ .... }
}
template<class T> class Handle{
Pool<T>* pool_; //pointer back to container
int pool_index_; //where I am in the container
T* operator->() {
return pool_->v.at(pool_index_); //i.e. "pool[index]"
}
void destroy(){
pool_-> ... destroy(this) .... mark "pool_index_" as unused, etc ....
}
}
现在我希望 Handle<> 支持多态性。
问题
很多高手都好心建议我用weak_ptr,可是我还是空白了一个星期,不知道怎么做。
我卡住的主要部分是:-
应该create()returnweak_ptr,而不是句柄? ....或者应该 Handle 封装 weak_ptr?
如果 create() return weak_ptr 对于用户程序,.. . weak_ptr 怎么知道 pool_index_?它没有这样的字段。
如果用户将 weak_ptr/Handle 转换为父 class 指针如下,还有很多问题:-
例如
class B{}
class C : public B { ......
}
....
{
Pool<C> cs;
Handle<C> cPtr=cs.create();
Handle<B> bPtr=cPtr; // casting ;expected to be valid,
// ... but how? (weak_ptr may solve it)
bPtr->destroy() ; // aPtr will invoke Pool<B>::destroy which is wrong!
// Pool<C>::destroy is the correct one
bPtr.operator->() ; // face the same problem as above
}
假设
- Pool 总是在 Handle 之后删除(为简单起见)。
- 没有多线程
这里有类似的问题,但 none 足够接近了。
C++11 memory pool design pattern?
关于weak_ptr
A std::weak_ptr
总是与 std::shared_ptr
关联。要使用 weak_ptr
,您必须使用 shared_ptr
来管理您的对象。这意味着您的对象的所有权可以共享:任何人都可以从 weak_ptr
构造一个 shared_ptr
并将其存储在某个地方。只有当所有 shared_ptr
都被销毁时,指向的对象才会被删除。 Pool
将失去对对象释放的直接控制,因此无法支持 public destroy()
函数。
使用共享所有权,事情会变得非常混乱。
这就是为什么 std::unique_ptr
通常是对象生命周期管理的更好选择的原因之一(遗憾的是它不适用于 weak_ptr
)。您的 Handle::destroy()
函数还暗示这不是您想要的,并且 Pool
应该单独处理其对象的生命周期。
但是,shared_ptr
/weak_ptr
是为多线程应用程序设计的。在单线程环境中,您可以获得类似 weak_ptr
的功能(检查有效目标并避免悬挂指针),而无需使用 weak_ptr
:
template<class T> class Pool {
bool isAlive(int index) const { ... }
}
template<class T> class Handle {
explicit operator bool() const { return pool_->isAlive(pool_index_); }
}
为什么这只适用于单线程环境?
在多线程程序中考虑这个场景:
void doSomething(std::weak_ptr<Obj> weak) {
std::shared_ptr<Obj> shared = weak.lock();
if(shared) {
// Another thread might destroy the object right here
// if we didn't have our own shared_ptr<Obj>
shared->doIt(); // And this would crash
}
}
在上面的例子中,我们必须确保指向的对象在if()
之后仍然可以访问。因此,我们构建了一个 shared_ptr
来让它保持活力——无论如何。
在单线程程序中你不必担心:
void doSomething(Handle<Obj> handle) {
if(handle) {
// No other threads can interfere
handle->doIt();
}
}
多次取消引用句柄时仍然要小心。示例:
void doDamage(Handle<GameUnit> source, Handle<GameUnit> target) {
if(source && target) {
source->invokeAction(target);
// What if 'target' reflects some damage back and kills 'source'?
source->payMana(); // Segfault
}
}
但使用另一个 if(source)
,您现在可以轻松检查句柄是否仍然有效!
铸造手柄
因此,Handle<T>
中的模板参数 T
不一定与池的类型匹配。也许你可以用模板魔法解决这个问题。我只能想出一个使用动态调度(虚拟方法调用)的解决方案:
struct PoolBase {
virtual void destroy(int index) = 0;
virtual void* get(int index) = 0;
virtual bool isAlive(int index) const = 0;
};
template<class T> struct Pool : public PoolBase {
Handle<T> create() { return Handle<T>(this, nextIndex); }
void destroy(int index) override { ... }
void* get(int index) override { ... }
bool isAlive(int index) const override { ... }
};
template<class T> struct Handle {
PoolBase* pool_;
int pool_index_;
Handle(PoolBase* pool, int index) : pool_(pool), pool_index_(index) {}
// Conversion Constructor
template<class D> Handle(const Handle<D>& orig) {
T* Cannot_cast_Handle = (D*)nullptr;
(void)Cannot_cast_Handle;
pool_ = orig.pool_;
pool_index_ = orig.pool_index_;
}
explicit operator bool() const { return pool_->isAlive(pool_index_); }
T* operator->() { return static_cast<T*>( pool_->get(pool_index_) ); }
void destroy() { pool_->destroy(pool_index_); }
};
用法:
Pool<Impl> pool;
Handle<Impl> impl = pool.create();
// Conversions
Handle<Base> base = impl; // Works
Handle<Impl> impl2 = base; // Compile error - which is expected
检查有效转换的行可能会被优化掉。检查仍将在编译时进行!尝试无效转换会给您这样的错误:
error: invalid conversion from 'Base*' to 'Impl*' [-fpermissive]
T* Cannot_cast_Handle = (D*)nullptr;
我在这里上传了一个简单的、可编译的测试用例:http://ideone.com/xeEdj5