在过去的两年里,我在我的项目中广泛使用了智能指针(确切地说是 boost::shared_ptr)。 我理解并欣赏它们的好处,而且我通常非常喜欢它们。 但我使用它们越多,我就越怀念 C++ 在内存管理和 RAII 方面的确定性行为,而我似乎很喜欢这种编程语言。 智能指针简化了内存管理过程,并提供自动垃圾收集等功能,但问题是,一般情况下使用自动垃圾收集和智能指针会在初始化(取消)顺序上引入某种程度的不确定性。 这种不确定性剥夺了程序员的控制权,并且正如我最近意识到的那样,使得设计和开发 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 易于正确使用且难以错误使用是一个好主意。 所以我问你们。 你怎么认为? 我应该如何缓解这个问题? 你会如何设计这样一个系统?
提前致谢,
乔什
I've been extensively using smart pointers (boost::shared_ptr to be exact) in my projects for the last two years. I understand and appreciate their benefits and I generally like them a lot. But the more I use them, the more I miss the deterministic behavior of C++ with regarding to memory management and RAII that I seem to like in a programming language. Smart pointers simplify the process of memory management and provide automatic garbage collection among other things, but the problem is that using automatic garbage collection in general and smart pointer specifically introduces some degree of indeterminisim in the order of (de)initializations. This indeterminism takes the control away from the programmers and, as I've come to realize lately, makes the job of designing and developing APIs, the usage of which is not completely known in advance at the time of development, annoyingly time-consuming because all usage patterns and corner cases must be well thought of.
To elaborate more, I'm currently developing an API. Parts of this API requires certain objects to be initialized before or destroyed after other objects. Put another way, the order of (de)initialization is important at times. To give you a simple example, let's say we have a class called System. A System provides some basic functionality (logging in our example) and holds a number of Subsystems via smart pointers.
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
};
As you can already tell, a Subsystem is only meaningful in the context of a System. But a Subsystem in such a design can easily outlive its parent System.
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;
}
If we had used raw pointers to hold subsystems, we would have destroyed subsystems when our system had gone down, of course then, pSomeSubsystem would be a dangling pointer.
Although, it's not the job of an API designer to protect the client programmers from themselves, it's a good idea to make the API easy to use correctly and hard to use incorrectly. So I'm asking you guys. What do you think? How should I alleviate this problem? How would you design such a system?
Thanks in advance,
Josh
发布评论
评论(9)
问题摘要
这个问题有两个相互竞争的问题。
子系统
的客户端需要知道他们正在使用的子系统
是有效的。处理 #1
System
拥有Subsystem
并应在其自己的范围内管理其生命周期。 为此使用shared_ptr特别有用,因为它简化了销毁,但您不应该分发它们,因为这样您就失去了在释放它们时所寻求的确定性。处理#2
这是需要解决的更有趣的问题。 更详细地描述问题,您需要客户端接收一个行为类似于子系统的对象,而该子系统(及其父系统)存在,但在
子系统
被销毁后表现正常。通过组合代理模式可以轻松解决这个问题状态模式 和 空对象模式。 虽然这似乎是一个有点复杂的解决方案,“只有在复杂性的另一面才能拥有简单性”。 作为库/API 开发人员,我们必须付出更多努力来使我们的系统变得健壮。 此外,我们希望我们的系统能够像用户期望的那样直观地运行,并在他们试图滥用它们时优雅地衰退。 这个问题有很多解决方案,但是,这个解决方案应该让您到达所有重要的一点,正如您和 Scott Meyers 说,“正确使用很容易,错误使用很难。”
现在,我假设实际上,
System
处理Subsystem
的一些基类,您可以从中派生出各种不同的Subsystem
,我在下面介绍了它。SubsystemBase
。下面需要引入一个Proxy对象,SubsystemProxy
,它实现了SubsystemBase的接口。
通过将请求转发到它所代理的对象(从这个意义上说,它非常像 装饰器模式。)每个子系统
创建这些对象之一,并通过shared_ptr
保存该对象,并在通过GetProxy()
请求时返回,调用GetSubsystem()
时由父System
对象调用。当
System
超出范围时,它的每个Subsystem
对象都会被破坏。 在它们的析构函数中,它们调用mProxy->Nullify()
,这会导致它们的 Proxy 对象更改它们的 State 。 他们通过更改为指向空对象来实现此目的,该对象实现SubsystemBase
接口,但不执行任何操作。此处使用状态模式允许客户端应用程序完全不知道特定
子系统
是否存在。 此外,它不需要检查指针或保留应该被销毁的实例。代理模式允许客户端依赖于一个轻量级对象,该对象完全封装了 API 内部工作的细节,并维护一个恒定、统一的接口。
空对象模式允许代理在原始
子系统
完成后运行已删除。示例代码
我在这里放置了一个粗略的伪代码质量示例,但我对此并不满意。 我将其重写为我上面描述的精确的编译(我使用 g++)示例。 为了让它工作,我必须引入一些其他类,但它们的用途应该从它们的名称中清楚地看出。 我为
NullSubsystem 使用了单例模式 code> 类,因为您不需要多个类,这是有道理的。
ProxyableSubsystemBase
将代理行为从Subsystem
中完全抽象出来,使其能够忽略此行为。 以下是类的 UML 图:示例代码:
代码输出:
其他想法:
我在《游戏编程宝石》一书中读到的一篇有趣的文章讨论了如何使用 Null用于调试和开发的对象。 他们特别讨论了使用空图形模型和纹理,例如棋盘纹理,以使缺失的模型真正脱颖而出。 通过将
NullSubsystem
更改为ReportingSubsystem
也可以应用同样的方法,该子系统将记录调用,并可能在访问时记录调用堆栈。 这将允许您或您的图书馆的客户根据超出范围的内容来追踪他们所在的位置,但不需要导致崩溃。我在@Arkadiy 的评论中提到,他在
System
和Subsystem
之间提出的循环依赖有点令人不快。 通过让System
派生自Subsystem
所依赖的接口(Robert C Martin 的 依赖倒置原则。 更好的方法是将子系统所需的功能与其父系统隔离,为其编写一个接口,然后在系统中保留该接口的实现者并将其传递给子系统
,它将通过shared_ptr
保存它。 例如,您可能有LoggerInterface
,您的Subsystem
使用它来写入日志,然后您可以派生CoutLogger
或FileLogger< /code> ,并在
System
中保留一个此类实例。Problem Summary
There are two competing concerns in this question.
Subsystem
s, allowing their removal at the right time.Subsystem
s need to know that theSubsystem
they are using is valid.Handling #1
System
owns theSubsystem
s and should manage their life-cycle with it's own scope. Usingshared_ptr
s for this is particularly useful as it simplifies destruction, but you should not be handing them out because then you loose the determinism you are seeking with regard to their deallocation.Handling #2
This is the more intersting concern to address. Describing the problem in more detail, you need clients to receive an object which behaves like a
Subsystem
while thatSubsystem
(and it's parentSystem
) exists, but behaves appropriately after aSubsystem
is destroyed.This is easily solved by a combination of the Proxy Pattern, the State Pattern and the Null Object Pattern. While this may seem to be a bit complex of a solution, 'There is a simplicity only to be had on the other side of complexity.' As Library/API developers, we must go the extra mile to make our systems robust. Further, we want our systems to behave intuitively as a user expects, and to decay gracefully when they attempt to misuse them. There are many solutions to this problem, however, this one should get you to that all important point where, as you and Scott Meyers say, it is "easy to use correctly and hard to use incorrectly.'
Now, I am assuming that in reality,
System
deals in some base class ofSubsystem
s, from which you derive various differentSubsystem
s. I've introduced it below asSubsystemBase
. You need to introduce a Proxy object,SubsystemProxy
below, which implements the interface ofSubsystemBase
by forwarding requests to the object it is proxying. (In this sense, it is very much like a special purpose application of the Decorator Pattern.) EachSubsystem
creates one of these objects, which it holds via ashared_ptr
, and returns when requested viaGetProxy()
, which is called by the parentSystem
object whenGetSubsystem()
is called.When a
System
goes out of scope, each of it'sSubsystem
objects gets destructed. In their destructor, they callmProxy->Nullify()
, which causes their Proxy objects to change their State. They do this by changing to point to a Null Object, which implements theSubsystemBase
interface, but does so by doing nothing.Using the State Pattern here has allowed the client application to be completely oblivious to whether or not a particular
Subsystem
exists. Moreover, it does not need to check pointers or keep around instances that should have been destroyed.The Proxy Pattern allows the client to be dependent on a light weight object that completely wraps up the details of the API's inner workings, and maintains a constant, uniform interface.
The Null Object Pattern allows the Proxy to function after the original
Subsystem
has been removed.Sample Code
I had placed a rough pseudo-code quality example here, but I wasn't satisfied with it. I've rewritten it to be a precise, compiling (I used g++) example of what I have described above. To get it to work, I had to introduce a few other classes, but their uses should be clear from their names. I employed the Singleton Pattern for the
NullSubsystem
class, as it makes sense that you wouldn't need more than one.ProxyableSubsystemBase
completely abstracts the Proxying behavior away from theSubsystem
, allowing it to be ignorant of this behavior. Here is the UML Diagram of the classes:Example Code:
Output from the code:
Other Thoughts:
An interesting article I read in one of the Game Programming Gems books talks about using Null Objects for debugging and development. They were specifically talking about using Null Graphics Models and Textures, such as a checkerboard texture to make missing models really stand out. The same could be applied here by changing out the
NullSubsystem
for aReportingSubsystem
which would log the call and possibly the callstack whenever it is accessed. This would allow you or your library's clients to track down where they are depending on something that has gone out of scope, but without the need to cause a crash.I mentioned in a comment @Arkadiy that the circular dependency he brought up between
System
andSubsystem
is a bit unpleasant. It can easily be remedied by havingSystem
derive from an interface on whichSubsystem
depends, an application of Robert C Martin's Dependency Inversion Principle. Better still would be to isolate the functionality thatSubsystem
s need from their parent, write an interface for that, then hold onto an implementor of that interface inSystem
and pass it to theSubsystem
s, which would hold it via ashared_ptr
. For example, you might haveLoggerInterface
, which yourSubsystem
uses to write to the log, then you could deriveCoutLogger
orFileLogger
from it, and keep an instance of such inSystem
.通过正确使用weak_ptr 类可以做到这一点。 事实上,您已经非常接近找到一个好的解决方案了。 你是对的,你不能期望你“超越”你的客户端程序员,你也不应该期望他们总是遵循你的 API 的“规则”(我相信你已经知道了)。 因此,您真正能做的最好的事情就是控制损害。
我建议您对
GetSubsystem
的调用返回一个weak_ptr
而不是shared_ptr
,这样客户端开发人员就可以测试指针的有效性,而不必总是这样做。声称引用它。同样,将
pParentSystem
设为boost::weak_ptr
,以便它可以通过调用在内部检测其父System
是否仍然存在pParentSystem
上的lock
并检查NULL
(原始指针不会告诉您这一点)。假设您更改
Subsystem
类以始终检查其对应的System
对象是否存在,您可以确保如果客户端程序员尝试使用Subsystem 对象超出了将导致错误(由您控制)的预期范围,而不是莫名其妙的异常(您必须相信客户端程序员能够捕获/正确处理)。
因此,在您的
main()
示例中,事情不会进展顺利! 在Subsystem
的 dtor 中处理这个问题的最优雅的方法是让它看起来像这样:我希望这有帮助!
This is do-able with proper use of the
weak_ptr
class. In fact, you are already quite close to having a good solution. You are right that you cannot be expected to "out-think" your client programmers, nor should you expect that they will always follow the "rules" of your API (as I'm sure you are already aware). So, the best you can really do is damage control.I recommend having your call to
GetSubsystem
return aweak_ptr
rather than ashared_ptr
simply so that the client developer can test the validity of the pointer without always claiming a reference to it.Similarly, have
pParentSystem
be aboost::weak_ptr<System>
so that it can internally detect whether its parentSystem
still exists via a call tolock
onpParentSystem
along with a check forNULL
(a raw pointer won't tell you this).Assuming you change your
Subsystem
class to always check whether or not its correspondingSystem
object exists, you can ensure that if the client programmer attempts to use theSubsystem
object outside of the intended scope that an error will result (that you control), rather than an inexplicable exception (that you must trust the client programmer to catch/properly handle).So, in your example with
main()
, things won't go BOOM! The most graceful way to handle this in theSubsystem
's dtor would be to have it look something like this:I hope this helps!
在这里,系统显然拥有子系统,我认为共享所有权没有任何意义。 我只是返回一个原始指针。 如果子系统的寿命比其系统的寿命长,那么它本身就是一个错误。
Here System clearly owns the subsystems and I see no point in having shared ownership. I would simply return a raw pointer. If a Subsystem outlives its System, that's an error on its own.
你在第一段的一开始就说对了。
您基于 RAII 的设计(如我的设计和大多数编写良好的 C++ 代码)要求您的对象由独占所有权指针持有。 在 Boost 中,这将是scoped_ptr。
那么为什么不使用scoped_ptr呢? 这肯定是因为您希望利用weak_ptr的好处来防止悬空引用,但您只能将weak_ptr指向shared_ptr。 因此,当您真正想要的是单一所有权时,您采用了通常的做法,即方便地声明shared_ptr。 这是一个错误的声明,正如您所说,它会损害以正确的顺序调用析构函数。 当然,如果您从不共享所有权,您将侥幸逃脱 - 但您必须不断检查所有代码以确保它从未被共享。
更糟糕的是 boost::weak_ptr 使用起来不方便(它没有 -> 运算符),因此程序员通过错误地将被动观察引用声明为 share_ptr 来避免这种不便。 这当然共享所有权,如果您忘记将shared_ptr清空,那么您的对象将不会被销毁,也不会在您想要的时候调用它的析构函数。
简而言之,您已经被 boost 库欺骗了 - 它没有接受良好的 C++ 编程实践,并迫使程序员做出错误的声明,以便尝试从中获得一些好处。 它仅适用于编写真正想要共享所有权并且对严格控制内存或以正确顺序调用析构函数不感兴趣的粘合代码脚本。
我也曾走过和你一样的道路。 C++ 中迫切需要防止悬空指针,但 boost 库没有提供可接受的解决方案。 我必须解决这个问题 - 我的软件部门希望保证 C++ 的安全。 所以我自己做了 - 这是相当多的工作,可以在以下位置找到:
http ://www.codeproject.com/KB/cpp/XONOR.aspx
对于单线程工作来说它完全足够了,我即将更新它以支持跨线程共享的指针。 其主要特点是支持专有对象的智能(自归零)被动观察者。
不幸的是,程序员已经被垃圾收集和“一刀切”的智能指针解决方案所诱惑,并且在很大程度上甚至没有考虑所有权和被动观察者 - 结果他们甚至不知道他们正在做的事情是错误的并且不知道不要抱怨。 针对 Boost 的异端几乎闻所未闻!
向您建议的解决方案非常复杂,而且没有任何帮助。 它们是由于文化上不愿意承认对象指针具有必须正确声明的独特角色以及盲目相信 Boost 必须是解决方案而导致的荒谬例子。
You were right at the very beginning in your first paragraph.
Your designs based on RAII (like mine and most well written C++ code) require that your objects are held by exclusive ownership pointers. In Boost that would be scoped_ptr.
So why didn't you use scoped_ptr. It will certainly be because you wanted the benefits of weak_ptr to protect against dangling references but you can only point a weak_ptr at a shared_ptr. So you have adopted the common practice of expediently declaring shared_ptr when what you really wanted was single ownership. This is a false declaration and as you say, it compromises destructors being called in the correct sequence. Of course if you never ever share the ownership you will get away with it - but you will have to constantly check all of your code to make sure it was never shared.
To make matters worse the boost::weak_ptr is inconvenient to use (it has no -> operator) so programmers avoid this inconvenience by falsely declaring passive observing references as shared_ptr. This of course shares ownership and if you forget to null that shared_ptr then your object will not get destroyed or its destructor called when you intend it to.
In short, you have been shafted by the boost library - it fails to embrace good C++ programming practices and forces programmers to make false declarations in order to try and derive some benefit from it. It is only useful for scripting glue code that genuinely wants shared ownership and is not interested in tight control over memory or destructors being called in the correct sequence.
I have been down the same path as you. Protection against dangling pointers is badly needed in C++ but the boost library does not provide an acceptable solution. I had to solve this problem - my software department wanted assurances that C++ can be made safe. So I rolled my own - it was quite a lot of work and can be found at:
http://www.codeproject.com/KB/cpp/XONOR.aspx
It is totally adequate for single threaded work and I am about to update it to embrace pointers being shared across threads. Its key feature is that it supports smart (self-zeroing) passive observers of exclusively owned objects.
Unfortunately programmers have become seduced by garbage collection and 'one size fits all' smart pointer solutions and to a large extent are not even thinking about ownership and passive observers - as a result they do not even know that what they are doing is wrong and don't complain. Heresy against Boost is almost unheard of!
The solutions that have been suggested to you are absurdly complicated and of no help at all. They are examples of the absurdity that results from a cultural reluctance to recognize that object pointers have distinct roles that must be correctly declared and a blind faith that Boost must be the solution.
我不认为让 System::GetSubsystem 返回指向子系统的原始指针(而不是智能指针)有问题。 由于客户端不负责构造对象,因此不存在客户端负责清理的隐式契约。 由于它是内部引用,因此可以合理地假设子系统对象的生命周期依赖于系统对象的生命周期。 然后,您应该通过文件说明这一隐含合同。
关键是,您没有重新分配或共享所有权 - 那么为什么要使用智能指针呢?
I don't see a problem with having System::GetSubsystem return a raw pointer (RATHER than a smart pointer) to a Subsystem. Since the client is not responsible for constructing the objects, then there is no implicit contract for the client to be responsible for the cleanup. And since it is an internal reference, it should be reasonable to assume that the lifetime of the Subsystem object is dependent on the lifetime of the System object. You should then reinforce this implied contract with documentation stating as much.
The point is, that you are not reassigning or sharing ownership - so why use a smart pointer?
这里真正的问题是你的设计。 没有很好的解决方案,因为该模型没有反映良好的设计原则。 这是我使用的一个方便的经验法则:
我意识到你的例子是人为的,但它是我在工作中经常看到的一种反模式。 问问自己,加上
std::vector
不是吗? API 的用户需要了解System
的值是什么? shared_ptr<子系统> >SubSystem
的接口(因为您返回了它们),因此为它们编写持有者只会增加复杂性。 至少人们知道std::vector
的接口,迫使他们记住at()
上面的GetSubsystem()
或operator[ ]
只是意思。你的问题是关于管理对象的生命周期,但是一旦你开始分发对象,你要么会因为允许其他人保持它们的活动而失去对生命周期的控制(
shared_ptr
),或者如果在它们被使用后使用它们,则会面临崩溃的风险。消失了(原始指针)。 在多线程应用程序中,情况更糟 - 谁锁定了您分发给不同线程的对象? 当以这种方式使用时,增强共享指针和弱指针会导致复杂性陷阱,特别是因为它们的线程安全性足以让没有经验的开发人员陷入困境。如果您要创建一个持有者,它需要向您的用户隐藏复杂性,并减轻他们可以自己管理的负担。 例如,一个接口由以下部分组成:a) 向子系统发送命令(例如 URI - /system/subsystem/command?param=value)和 b) 迭代子系统和子系统命令(通过类似 stl 的迭代器)以及可能的 c)注册子系统将允许您向用户隐藏几乎所有的实现细节,并在内部强制执行生命周期/排序/锁定要求。
在任何情况下,可迭代/可枚举的 API 都比公开对象更可取 - 命令/注册可以轻松序列化以生成测试用例或配置文件,并且可以交互式地显示它们(例如,在树形控件中,带有由查询组成的对话框)可用的操作/参数)。 您还可以保护您的 API 用户免受您可能需要对子系统类进行的内部更改的影响。
我会警告您不要遵循亚伦回答中的建议。 为如此简单的问题设计一个解决方案,需要 5 种不同的设计模式来实现,这只意味着解决了错误的问题。 我也厌倦了任何人在设计方面引用迈尔斯先生的话,因为他自己承认:
“我已经 20 多年没有编写过生产软件了,而且我从来没有用 C++ 编写过生产软件。不,从来没有。此外,我什至从未尝试过用 C++ 编写生产软件,所以我不仅不是真正的 C++ 开发人员,而且我什至不是一个想成为 C++ 开发者的人,稍微平衡一下这一点的是我在研究生期间确实用 C++ 编写了研究软件。学年(1985-1993),但即便如此,对于单个开发人员来说,这些内容也很小(几千行),很快就会被抛弃。自从十几年前开始担任顾问以来,我的 C++ 编程已经发生了变化。仅限于玩具“让我们看看它是如何工作的”(或者,有时,“让我们看看这会破坏多少个编译器”)程序,通常是适合单个文件的程序”。
并不是说他的书不值得一读,但他没有权力谈论设计或复杂性。
The real problem here is your design. There is no nice solution, because the model doesn't reflect good design principles. Here's a handy rule of thumb I use:
I realise that your example is contrived, but its an anti-pattern I see a lot at work. Ask yourself, what value is
System
adding thatstd::vector< shared_ptr<SubSystem> >
doesnt? Users of your API need to know the interface ofSubSystem
(since you return them), so writing a holder for them is only adding complexity. At least people know the interface tostd::vector
, forcing them to rememberGetSubsystem()
aboveat()
oroperator[]
is just mean.Your question is about managing object lifetimes, but once you start handing out objects, you either lose control of the lifetime by allowing others to keep them alive (
shared_ptr
) or risk crashes if they are used after they have gone away (raw pointers). In multi-threaded apps its even worse - who locks the objects you are handing out to different threads? Boosts shared and weak pointers are a complexity inducing trap when used in this fashion, especially since they are just thread safe enough to trip up inexperienced developers.If you are going to create a holder, it needs to hide complexity from your users and relieve them of burdens you can manage yourself. As an example, an interface consisting of a) Send command to subsystem (e.g. a URI - /system/subsystem/command?param=value) and b) iterate subsystems and subsystem commands (via an stl-like iterator) and possibly c) register subsystem would allow you to hide almost all of the details of your implementation from your users, and enforce the lifetime/ordering/locking requirements internally.
An iteratable/enumerable API is vastly preferable to exposing objects in any case - the commands/registrations could be easily serialised for generating test cases or configuration files, and they could be displayed interactively (say, in a tree control, with dialogs composed by querying the available actions/parameters). You would also be protecting your API users from internal changes you may need to make to the subsystem classes.
I would caution you against following the advice in Aarons answer. Designing a solution to an issue this simple that requires 5 different design patterns to implement can only mean that the wrong problem is being solved. I'm also weary of anyone who quotes Mr Myers in relation to design, since by his own admission:
"I have not written production software in over 20 years, and I have never written production software in C++. Nope, not ever. Furthermore, I’ve never even tried to write production software in C++, so not only am I not a real C++ developer, I’m not even a wannabe. Counterbalancing this slightly is the fact that I did write research software in C++ during my graduate school years (1985-1993), but even that was small (a few thousand lines) single-developer to-be-thrown-away-quickly stuff. And since striking out as a consultant over a dozen years ago, my C++ programming has been limited to toy “let’s see how this works” (or, sometimes, “let’s see how many compilers this breaks”) programs, typically programs that fit in a single file".
Not to say that his books aren't worth reading, but he has no authority to speak on design or complexity.
在您的示例中,如果系统持有
vector
而不是vector 会更好。 >
。 它既简单又消除您的担忧。 GetSubsystem 将返回一个引用。In your example, it would be better if the System held a
vector<Subsystem>
rather than avector<shared_ptr<Subsystem> >
. Its both simpler, and eliminates the concern you have. GetSubsystem would return a reference instead.堆栈对象将以与实例化相反的顺序释放,因此除非使用 API 的开发人员尝试管理智能指针,否则通常不会成为问题。 有些事情你无法阻止,你能做的最好的事情就是在运行时提供警告,最好只进行调试。
你的例子对我来说看起来非常像COM,你对使用shared_ptr返回的子系统进行了引用计数,但是你在系统对象本身上缺少它。
如果每个子系统对象在创建时对系统对象执行 addref 并在销毁时释放,则如果系统对象提前销毁时引用计数不正确,则至少可以显示异常。
使用weak_ptr还可以让你提供一条消息,而不是在以错误的顺序释放东西时爆炸。
Stack objects will be released in the opposite order to which they where instantiated, so unless the developer using the API is trying to manage the smart pointer, it's normally not going to be a problem. There are just some things you are not going to be able to prevent, the best you can do is provide warnings at run time, preferably debug only.
Your example seems very much like COM to me, you have reference counting on the subsystems being returned by using shared_ptr, but you are missing it on the system object itself.
If each of the subsystem objects did an addref on the system object on creation, and a release on destruction you could at least display an exception if the reference count was incorrect when the system object is destroyed early.
Use of weak_ptr would also allow you to provide a message instead/aswell as blowing up when things are freed in the wrong order.
你的问题的本质是循环引用:系统引用子系统,而子系统又引用系统。 这种数据结构无法通过引用计数轻松处理 - 它需要适当的垃圾收集。 您试图通过对边缘之一使用原始指针来打破循环 - 这只会产生更多的复杂性。
至少已经提出了两个好的解决方案,因此我不会尝试超越以前的海报。 我只能指出,在@Aaron 的解决方案中,您可以拥有系统代理而不是子系统代理 - 取决于更复杂的内容和有意义的内容。
The essence of your problem is a circular reference: System refers to Subsystem, and Subsystem, in turn, refers to System. This kind of data structure cannot be easily handled by reference counting - it requires proper garbage collection. You're trying to break the loop by using a raw pointer for one of the edges - this will only produce more complications.
At least two good solutions have been suggested, so I will not attempt to outdo the previous posters. I can only note that in @Aaron's solution you can have a proxy for the System rather than for Subsystems - dependingo n what is more complex and what makes sense.