C++ 关于智能指针及其不可避免的不确定性的问题
在过去的两年里,我一直在我的项目中广泛使用智能指针(确切地说是boost::shared_ptr)。我理解并欣赏他们的好处,我通常非常喜欢他们。但是我越多地使用它们,我就越不关心C++的确定行为,因为我对程序管理语言中的内存管理和RAII很感兴趣。智能指针简化了内存管理过程,并提供了自动垃圾收集等功能,但问题是,通常使用自动垃圾收集和智能指针会在(取消)初始化的顺序上引入某种程度的不确定性。这种不确定性夺走了程序员的控制权,而且,正如我最近意识到的那样,使得设计和开发API的工作变得非常耗时,因为在开发时,API的使用还没有完全被预先知道,因为所有的使用模式和角落案例都必须被仔细考虑 更详细地说,我目前正在开发一个API。此API的某些部分要求在其他对象之前初始化某些对象,或在其他对象之后销毁某些对象。换句话说,(反)初始化的顺序有时很重要。给你一个简单的例子,假设我们有一个叫做System的类。系统提供一些基本功能(在我们的示例中是日志记录),并通过智能指针保存许多子系统C++ 关于智能指针及其不可避免的不确定性的问题,c++,shared-ptr,raii,object-lifetime,C++,Shared Ptr,Raii,Object Lifetime,在过去的两年里,我一直在我的项目中广泛使用智能指针(确切地说是boost::shared_ptr)。我理解并欣赏他们的好处,我通常非常喜欢他们。但是我越多地使用它们,我就越不关心C++的确定行为,因为我对程序管理语言中的内存管理和RAII很感兴趣。智能指针简化了内存管理过程,并提供了自动垃圾收集等功能,但问题是,通常使用自动垃圾收集和智能指针会在(取消)初始化的顺序上引入某种程度的不确定性。这种不确定性夺走了程序员的控制权,而且,正如我最近意识到的那样,使得设计和开发API的工作变得非常耗时,因
class System {
public:
boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
assert( index < mSubsystems.size() );
return mSubsystems[ index ];
}
void LogMessage( const std::string& message ) {
std::cout << message << std::endl;
}
private:
typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
SubsystemList mSubsystems;
};
class Subsystem {
public:
Subsystem( System* pParentSystem )
: mpParentSystem( pParentSystem ) {
}
~Subsystem() {
pParentSubsystem->LogMessage( "Destroying..." );
// Destroy this subsystem: deallocate memory, release resource, etc.
}
/*
Other stuff here
*/
private:
System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};
类系统{
公众:
boost::shared_ptrGetSubsystem(无符号整数索引){
断言(索引LogMessage(“正在销毁…”);
//销毁此子系统:释放内存、释放资源等。
}
/*
这里还有其他东西
*/
私人:
System*pParentSystem;//避免循环的原始指针-也可以使用弱\u ptr
};
正如您已经知道的,子系统仅在系统的上下文中才有意义。但是,在这样的设计中,子系统可以很容易地比其父系统长寿
int main() {
{
boost::shared_ptr< Subsystem > pSomeSubsystem;
{
boost::shared_ptr< System > pSystem( new System );
pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );
} // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.
} // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!
return 0;
}
intmain(){
{
boost::shared_ptrpSomeSubsystem;
{
boost::shared_ptrpSystem(新系统);
pSomeSubsystem=pSystem->GetSubsystem(/*一些索引*/);
}//我们的系统将超出范围并在此处被销毁,但pSomeSubsystem指向的子系统不会被销毁。
}//pSomeSubsystem将超出此范围,但请稍候,我们如何在子系统的析构函数中记录消息?!它的父系统毕竟已被破坏。轰!
返回0;
}
如果我们使用原始指针来保存子系统,那么当我们的系统崩溃时,我们就会破坏子系统,当然,pSomeSubsystem将是一个悬空的指针
虽然API设计师的工作不是保护客户端程序员不受自己的影响,但让API易于正确使用和不易错误使用是一个好主意。所以我问你们。你们认为呢?我应该如何缓解这个问题?你们将如何设计这样一个系统
提前感谢,,
Josh在您的示例中,如果系统持有的是
向量,而不是向量,那就更好了。这既简单又消除了您的顾虑。GetSubsystem将返回一个引用。我认为System::GetSubsystem返回原始指针(而不是智能指针)没有问题对于子系统。由于客户端不负责构建对象,因此客户端不存在负责清理的隐式契约。而且由于它是一个内部引用,因此应该合理地假设子系统对象的生存期取决于系统对象的生存期。您应该hen通过文件说明,加强本默示合同
关键是,您没有重新分配或共享所有权-那么为什么要使用智能指针呢?在这里,系统显然拥有子系统,我认为共享所有权没有任何意义。我只会返回一个原始指针。如果子系统比它的系统寿命长,这本身就是一个错误。这可以通过正确使用弱属性来实现r
class。事实上,您已经非常接近于拥有一个好的解决方案。您是对的,您不能期望您的客户程序员“超越”您的客户程序员,也不应该期望他们总是遵循API的“规则”(我相信您已经知道)。因此,您真正能做的最好的事情是损害控制
我建议您调用GetSubsystem
返回weak\u ptr
而不是shared\u ptr
,这样客户端开发人员就可以测试指针的有效性,而不必总是声称引用它
类似地,让pParentSystem
成为一个boost::weak_ptr
,这样它就可以通过调用pParentSystem
上的lock
并检查NULL
(原始指针不会告诉你这一点)在内部检测其父系统是否仍然存在
假设您将子系统
类更改为始终检查其对应的系统
对象是否存在,则可以确保如果客户端程序员尝试在预期范围之外使用子系统
对象,则会导致错误(由您控制),而不是一个无法解释的异常(您必须相信客户端程序员能够捕获/正确处理)
因此,在您使用main()
的示例中,事情不会一帆风顺!在class Subsystem
{
...
~Subsystem() {
boost::shared_ptr<System> my_system(pParentSystem.lock());
if (NULL != my_system.get()) { // only works if pParentSystem refers to a valid System object
// now you are guaranteed this will work, since a reference is held to the System object
my_system->LogMessage( "Destroying..." );
}
// Destroy this subsystem: deallocate memory, release resource, etc.
// when my_system goes out of scope, this may cause the associated System object to be destroyed as well (if it holds the last reference)
}
...
};
#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>
// Forward Declarations to allow friending
class System;
class ProxyableSubsystemBase;
// Base defining the interface for Subsystems
class SubsystemBase
{
public:
// pure virtual functions
virtual void DoSomething(void) = 0;
virtual int GetSize(void) = 0;
virtual ~SubsystemBase() {} // virtual destructor for base class
};
// Null Object Pattern: an object which implements the interface to do nothing.
class NullSubsystem : public SubsystemBase
{
public:
// implements pure virtual functions from SubsystemBase to do nothing.
void DoSomething(void) { }
int GetSize(void) { return -1; }
// Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
static NullSubsystem *instance()
{
static NullSubsystem singletonInstance;
return &singletonInstance;
}
private:
NullSubsystem() {} // private constructor to inforce Singleton Pattern
};
// Proxy Pattern: An object that takes the place of another to provide better
// control over the uses of that object
class SubsystemProxy : public SubsystemBase
{
friend class ProxyableSubsystemBase;
public:
SubsystemProxy(SubsystemBase *ProxiedSubsystem)
: mProxied(ProxiedSubsystem)
{
}
// implements pure virtual functions from SubsystemBase to forward to mProxied
void DoSomething(void) { mProxied->DoSomething(); }
int GetSize(void) { return mProxied->GetSize(); }
protected:
// State Pattern: the initial state of the SubsystemProxy is to point to a
// valid SubsytemBase, which is passed into the constructor. Calling Nullify()
// causes a change in the internal state to point to a NullSubsystem, which allows
// the proxy to still perform correctly, despite the Subsystem going out of scope.
void Nullify()
{
mProxied=NullSubsystem::instance();
}
private:
SubsystemBase *mProxied;
};
// A Base for real Subsystems to add the Proxying behavior
class ProxyableSubsystemBase : public SubsystemBase
{
friend class System; // Allow system to call our GetProxy() method.
public:
ProxyableSubsystemBase()
: mProxy(new SubsystemProxy(this)) // create our proxy object
{
}
~ProxyableSubsystemBase()
{
mProxy->Nullify(); // inform our proxy object we are going away
}
protected:
boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }
private:
boost::shared_ptr<SubsystemProxy> mProxy;
};
// the managing system
class System
{
public:
typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;
SubsystemHandle GetSubsystem( unsigned int index )
{
assert( index < mSubsystems.size() );
return mSubsystems[ index ]->GetProxy();
}
void LogMessage( const std::string& message )
{
std::cout << " <System>: " << message << std::endl;
}
int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
{
LogMessage("Adding Subsystem:");
mSubsystems.push_back(SubsystemPtr(pSubsystem));
return mSubsystems.size()-1;
}
System()
{
LogMessage("System is constructing.");
}
~System()
{
LogMessage("System is going out of scope.");
}
private:
// have to hold base pointers
typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
SubsystemList mSubsystems;
};
// the actual Subsystem
class Subsystem : public ProxyableSubsystemBase
{
public:
Subsystem( System* pParentSystem, const std::string ID )
: mParentSystem( pParentSystem )
, mID(ID)
{
mParentSystem->LogMessage( "Creating... "+mID );
}
~Subsystem()
{
mParentSystem->LogMessage( "Destroying... "+mID );
}
// implements pure virtual functions from SubsystemBase
void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
int GetSize(void) { return sizeof(Subsystem); }
private:
System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
std::string mID;
};
//////////////////////////////////////////////////////////////////
// Actual Use Example
int main(int argc, char* argv[])
{
std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
System::SubsystemHandle H1;
System::SubsystemHandle H2;
std::cout << "-------------------------------------------" << std::endl;
{
std::cout << " main(): Begin scope for System." << std::endl;
System mySystem;
int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));
std::cout << " main(): Assigning Subsystems to H1 and H2." << std::endl;
H1=mySystem.GetSubsystem(FrankIndex);
H2=mySystem.GetSubsystem(ErnestIndex);
std::cout << " main(): Doing something on H1 and H2." << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << " main(): Leaving scope for System." << std::endl;
}
std::cout << "-------------------------------------------" << std::endl;
std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;
return 0;
}
main(): Creating Handles H1 and H2 for Subsystems.
-------------------------------------------
main(): Begin scope for System.
<System>: System is constructing.
<System>: Creating... Frank
<System>: Adding Subsystem:
<System>: Creating... Ernest
<System>: Adding Subsystem:
main(): Assigning Subsystems to H1 and H2.
main(): Doing something on H1 and H2.
<System>: Frank is DoingSomething (tm).
<System>: Ernest is DoingSomething (tm).
main(): Leaving scope for System.
<System>: System is going out of scope.
<System>: Destroying... Frank
<System>: Destroying... Ernest
-------------------------------------------
main(): Doing something on H1 and H2. (outside System Scope.)
main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.