在过去的两年里,我一直在我的项目中广泛使用智能指针(boost :: shared_ptr).我理解并欣赏他们的好处,我一般都喜欢他们.但是我越是使用它们,我就越想念C++的确定性行为,关于内存管理和RAII,我似乎更喜欢编程语言.智能指针简化了内存管理的过程并提供了自动垃圾收集等功能,但问题是一般使用自动垃圾收集和智能指针特别引入了(de)初始化顺序的某种程度的不确定性.这种不确定性使控制权远离程序员,并且正如我最近意识到的那样,它完成了设计和开发API的工作,
为了详细说明,我目前正在开发一个API.此API的某些部分要求在其他对象之前初始化或销毁某些对象.换句话说,(de)初始化的顺序有时很重要.举个简单的例子,假设我们有一个名为System的类.系统提供一些基本功能(在我们的示例中记录)并通过智能指针保存许多子系统.
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 };
正如您已经知道的那样,子系统仅在系统的上下文中有意义.但是,这种设计中的子系统可以轻松地超过其父系统.
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; }
如果我们使用原始指针来保存子系统,那么当我们的系统发生故障时我们就会破坏子系统,当然,pSomeSubsystem将是一个悬空指针.
虽然,API设计师的工作不是保护客户端程序员自己,但最好是使API易于正确使用且难以正确使用.所以我问你们.你怎么看?我该如何缓解这个问题?你会如何设计这样的系统?
先谢谢你,乔希
这个问题有两个相互竞争的问题.
Subsystem
s的生命周期管理,允许在适当的时间将其删除.
Subsystem
s的客户需要知道Subsystem
他们使用的是有效的.
System
拥有Subsystem
s并且应该用它自己的范围来管理它们的生命周期.shared_ptr
为此使用s特别有用,因为它简化了破坏,但你不应该将它们分发出来,因为那样你就会放弃你所寻求的关于它们释放的决定论.
这是需要解决的问题.更详细地描述问题,则需要客户端接收它的行为像一个对象Subsystem
,而Subsystem
(和它的母公司System
)存在,但表现一个后适当地Subsystem
被破坏.
通过代理模式,状态模式和空对象模式的组合可以很容易地解决这个问题.虽然这似乎有点复杂的解决方案," 复杂性的另一方面只有简单性." 作为图书馆/ API开发人员,我们必须加倍努力,使我们的系统更加健壮.此外,我们希望我们的系统能够像用户期望的那样直观地行动,并在他们试图滥用它们时优雅地衰减.对于这个问题,很多解决方案,但是,这应该让你所有重要的点,你和斯科特·迈尔斯说,它是" 易于正确使用,而难以错误使用. "
现在,我假设在现实中,System
在一些基类中进行交易,Subsystem
从中得出各种不同的Subsystem
s.我在下面介绍了它SubsystemBase
.您需要在下面引入一个Proxy对象,该对象通过将请求转发到它所代理的对象来SubsystemProxy
实现接口SubsystemBase
.(在这个意义上,它非常类似于Decorator模式的特殊用途应用程序.)每个Subsystem
创建一个这样的对象,它通过a保存shared_ptr
,并在请求via时返回GetProxy()
,在调用时由父System
对象GetSubsystem()
调用.
当一个System
超出范围时,它的每个Subsystem
对象都会被破坏.在他们的析构函数中,他们调用mProxy->Nullify()
,这导致他们的Proxy对象改变他们的状态.他们通过更改为指向实现接口的Null对象来执行此SubsystemBase
操作,但这样做无需执行任何操作.
在此处使用状态模式允许客户端应用程序完全忘记特定是否Subsystem
存在.而且,它不需要检查指针或保留应该被销毁的实例.
在代理模式允许客户端依赖完全包装了该API的内部工作的详细情况的重量轻的物体上,并保持恒定的,统一的接口.
该空对象模式允许代理原件后的功能Subsystem
已被删除.
我在这里放置了一个粗略的伪代码质量示例,但我对此并不满意.我把它重写为一个精确的,编译(我用g ++)我上面描述的例子.为了使它工作,我不得不介绍一些其他类,但他们的用途应该从他们的名字清楚.我在课堂上使用了Singleton模式NullSubsystem
,因为它不需要多于一个. ProxyableSubsystemBase
完全抽象出代理行为远离Subsystem
它,允许它对这种行为一无所知.这是类的UML图:
#include#include #include #include // 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 GetProxy() { return mProxy; } private: boost::shared_ptr 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 << " : " << 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 > 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 is constructing. : Creating... Frank : Adding Subsystem: : Creating... Ernest : Adding Subsystem: main(): Assigning Subsystems to H1 and H2. main(): Doing something on H1 and H2. : Frank is DoingSomething (tm). : Ernest is DoingSomething (tm). main(): Leaving scope for System. : System is going out of scope. : Destroying... Frank : 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.
我在Game Programming Gems的一本书中读到的一篇有趣的文章谈到了使用Null Objects进行调试和开发.他们特别谈论使用Null Graphics Models和Textures,例如棋盘格纹,以使缺失的模型真正脱颖而出.通过更改NullSubsystem
for ReportingSubsystem
会记录调用以及可能的callstack,只要访问它就可以应用同样的方法.这将允许您或您的图书馆的客户根据超出范围的内容追踪他们的位置,但不需要导致崩溃.
我在@Arkadiy评论中提到,他之间提出的循环依赖System
并且Subsystem
有点不愉快.通过System
从Subsystem
依赖的接口派生出来,罗伯特C马丁的依赖倒置原则的应用可以很容易地弥补它.更好的做法是将Subsystem
需要的功能与其父进程隔离,为其编写接口,然后保留该接口的实现者System
并将其传递给Subsystem
s,这将通过a保存它shared_ptr
.例如,你可能有LoggerInterface
,你Subsystem
用它写入日志,然后你可以从中派生CoutLogger
或FileLogger
从中,并保留这样的实例System
.
这可以通过正确使用weak_ptr
该类来实现.事实上,你已经非常接近于拥有一个好的解决方案.你是对的,你不能期望"超出想象"你的客户程序员,也不应该期望他们总是遵循你的API的"规则"(因为我相信你已经知道).所以,你真正做的最好的就是破坏控制.
我建议您调用GetSubsystem
返回weak_ptr
而不是shared_ptr
简单,以便客户端开发人员可以测试指针的有效性,而无需始终声明对它的引用.
类似地,它pParentSystem
是一个boost::weak_ptr
可以内部检测其父级是否System
仍然存在通过调用lock
on pParentSystem
以及检查NULL
(原始指针不会告诉你这一点).
假设您将Subsystem
类更改为始终检查其对应的System
对象是否存在,您可以确保如果客户端程序员尝试使用Subsystem
预期范围之外的对象(将由您控制),而不是一个无法解释的异常(您必须信任客户端程序员捕获/正确处理).
所以,在你的例子里main()
,事情不会是蠢货!在Subsystem
dtor中处理这个问题的最优雅的方法是让它看起来像这样:
class Subsystem { ... ~Subsystem() { boost::shared_ptrmy_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) } ... };
我希望这有帮助!