在过去的两年中,我在我的项目中广泛使用了智能指针(确切地说是boost::shared_ptr)。我理解并欣赏它们的好处,并且我通常非常喜欢它们。但是我使用它们的次数越多,我就越想念C++关于内存管理和RAII的确定性行为,而这种行为似乎是我喜欢用编程语言编写的。智能指针简化了内存管理过程,并提供了自动垃圾回收等功能,但是问题是,通常使用自动垃圾回收,而智能指针会按(反)初始化的顺序专门引入某种程度的不确定性。这种不确定性使控制权脱离了程序员,而且,正如我最近逐渐意识到的那样,它使得设计和开发API的工作变得困难,因为开发时尚未完全知道API的用法,这很烦人,因为必须仔细考虑所有使用方式和特殊情况。
为了详细说明,我目前正在开发API。此API的某些部分要求某些对象在其他对象之前初始化或销毁。换句话说,(取消)初始化的顺序有时很重要。举一个简单的例子,假设我们有一个名为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
的生命周期管理,允许在正确的时间删除它们。 Subsystem
的客户端需要知道他们使用的Subsystem
是有效的。 处理#1
System
拥有Subsystem
,应使用自己的作用域来管理其生命周期。为此使用shared_ptr
尤其有用,因为它可以简化销毁过程,但您不应将它们分发出去,因为这样一来,您就可以松开要寻找的关于其释放的确定性。处理#2
这是更有趣的问题。更详细地描述问题,您需要客户端接收一个对象,该对象在
Subsystem
(及其父Subsystem
)存在时的行为类似于System
,但在Subsystem
被销毁后仍可正常工作。这可以通过Proxy Pattern,State Pattern和Null Object Pattern的组合轻松解决。尽管这似乎是一个解决方案的复杂性,但“只有在复杂性的另一端才有一个简单性”。作为图书馆/API开发人员,我们必须付出更多努力以使我们的系统健壮。此外,我们希望我们的系统能够像用户期望的那样直观地表现,并在他们尝试滥用它们时优雅地衰减。这个问题有很多解决方案,但是,这应该使您到达所有重要的点,正如您和Scott Meyers所说,“容易正确使用,而难以错误使用”。
现在,我假设实际上,
System
处理Subsystem
的某些基类,您可以从中派生各种不同的Subsystem
。我在下面以SubsystemBase
的形式介绍了它。您需要在下面引入一个代理对象SubsystemProxy
,该对象通过将请求转发到要代理的对象来实现SubsystemBase
的接口(interface)。 (从这个意义上讲,它非常像Decorator Pattern的特殊用途应用程序。)每个Subsystem
都会创建其中一个对象,通过shared_ptr
持有该对象,并在通过GetProxy()
请求时返回,当父System
对象调用该方法时会返回GetSubsystem()
被调用。当
System
超出范围时,它的每个Subsystem
对象都会被破坏。在析构函数中,它们调用mProxy->Nullify()
,这会导致其代理对象更改其状态。他们通过更改为指向 Null对象来实现此目的,该对象实现了SubsystemBase
接口(interface),但没有执行任何操作。在这里使用状态模式允许客户端应用程序完全忽略是否存在特定的
Subsystem
。而且,它不需要检查指针或保留应该被销毁的实例。代理模式允许客户端依赖于轻量级对象,该对象完全包装了API内部工作的细节,并维护了一个恒定,统一的接口(interface)。
空对象模式允许代理在删除原始
Subsystem
后起作用。样例代码
我在这里放置了一个粗略的伪代码质量示例,但我对此并不满意。我已将其重写为上述内容的精确示例(我使用g++)。为了使其正常工作,我不得不引入其他一些类,但是从名称上应该清楚其用途。我为
NullSubsystem
类使用了Singleton Pattern,因为这样一来您就不需要多个。 ProxyableSubsystemBase
将Proxying行为完全从Subsystem
中抽象出来,从而使它可以忽略这种行为。这是这些类的UML图:示例代码:
#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.
其他想法:NullSubsystem
的ReportingSubsystem
可以在此处应用相同的代码,该System
会记录调用,并在每次访问时记录调用堆栈。这样一来,您或您的图书馆的客户就可以根据超出范围的内容来跟踪他们所处的位置,而无需造成崩溃。Subsystem
和System
之间提出的循环依赖关系有点令人不愉快。可以通过让Subsystem
从Subsystem
所依赖的接口(interface)派生而轻松地进行补救,这是Robert C Martin的Dependency Inversion Principle的应用。更好的方法是将System
所需的功能与其父级隔离,编写一个接口(interface),然后在Subsystem
中保留该接口(interface)的实现,并将其传递给shared_ptr
,后者将通过LoggerInterface
保留它。例如,您可能拥有Subsystem
,您的CoutLogger
使用FileLogger
将其写入日志,然后可以从其中导出System
或ojit_code,并将此类实例保留在ojit_code中。