一个问题上的明智指针及其不可避免的Indeterminism
-
03-07-2019 - |
题
我已经广泛使用智能指针(提高::情况是准确)在我的项目的最后两年。我的理解和赞赏他们的利益和我通常喜欢他们很多。但更多的我用它们,我错过确定性行为C++的有关内存管理和RAII,我似乎喜欢在一种编程语言。聪明的指针,简化过程的存储管理和提供自动垃圾收集除其他事项外,但问题是,使用自动垃圾收集一般和明智的指针,专门介绍某些程度的indeterminisim的顺序(de)的初始化。这indeterminism需要的控制从程序员,正如我已经认识到最近,使得工作的设计和发展中Api,使用的是不完全事先已知的时间发展,烦人耗费时间,因为所有的使用模式和角的情况下,必须深思熟虑。
拟订更明,我目前正在开发一个API。部分这API要求的某些目之前进行初始化或摧毁后,其它的对象。把另一种方式,秩序(de)的初始化是重要的。给你一个简单的例子,让我们说,我们有一类称为系统。一个系统提供了一些基本的功能(登录我们的例子),并持有量子系统通过的明智的指针。
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易于使用的正确和难以使用不正确。所以我问你们。你怎么想?我应该如何减轻这个问题?你会如何设计这一制度?
在此先感谢, Josh
解决方案
问题摘要
有两个相互竞争的关切在这个问题。
- 生命周期的管理
Subsystem
s,允许他们除在正确的时间。 - 客户
Subsystem
s需要知道的Subsystem
他们使用的是有效的。
处理#1
System
拥有 Subsystem
s和应当管理自己的生命周期与它自己的范围。使用 shared_ptr
s为这是特别有用,因为它简化了破坏,但是你应该不会交给他们,因为然后你失去的决定你都在寻求他们的释放。
处理#2
这是越野趣关切地址。描述问题的更详细,需要客户,以接收对象,它的行为就像一个 Subsystem
同时, Subsystem
(和它的父母 System
)是否存在,但行为适当之后 Subsystem
被破坏。
这很容易解决的一个组合 代理模式, , 国家图案 和 空对象的图案.虽然这可能看起来有点复杂的一个方案,'有一个简单只有在另一侧的复杂性.' 作为图书馆/API开发,我们必须加倍努力,以使我们的系统可靠。此外,我们希望我们的系统中的行为直观地作为用户的期望,并为衰变正常,当他们试图滥用他们。有很多这个问题的解决方案,但是,这个人应该得到你要的所有重要的一点,因为你和 梅尔*斯科特 说它是"易于使用的正确和难以使用不正确。'
现在,我假设,在现实中, System
交易在一些基类的 Subsystem
s你从中得出各种不同的 Subsystem
s.我已经介绍了这下面 SubsystemBase
.你需要引入一个 代理 目的, SubsystemProxy
下面,实现接口的 SubsystemBase
通过转发请求的对象是代理。(在这个意义上说,这是非常像一个特殊的目的应用 装饰图案.) 每 Subsystem
创建一个这些对象,它拥有通过一个 shared_ptr
, 和返回时要求通过 GetProxy()
, ,这就是所谓的父母 System
对象时 GetSubsystem()
被称为。
当一个 System
超出范围,每个这是 Subsystem
物体被毁灭。在他们析构,他们呼叫的 mProxy->Nullify()
, ,这会导致他们 代理 对象,以改变他们 状态.他们这样做是通过改变来点 Null Object, ,它实现了 SubsystemBase
接口,但是不会那么做没什么。
使用 国家图案 在这里,已经允许用户应用程序是完全无视,是否一个特别的 Subsystem
存在。而且,它不需要检查针或保持周围的情况下,应该已经被摧毁。
的 代理模式 允许客户以依赖于一个重量轻对象的完全包裹的详细资料API的内部运作,并保持恒定的,统一的接口。
的 空对象的图案 允许 代理 要功能之后,原始 Subsystem
已经删除。
代码样本
我已经把一个粗略的伪码质量的例子在这里,但我没有感到满意。我写它是一个精确的汇编(I使用的g++)如我所述。得到它的工作,我已经向大家介绍一些其他的类别,但它们的使用应当明确从他们的名字。我雇的 单独的图案 的 NullSubsystem
类,因为它是有道理的,你不需要超过一个。 ProxyableSubsystemBase
完全的摘要,代理行为离开 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
这将记录通话和可能的调用栈的时候访问。这会让你或者你的图书馆的客户追踪他们根据东西已经超出范围,但不需要到导致崩溃。我提到的注释@Arkadiy得圆形的依赖他带之间
System
和Subsystem
是有点不愉快。它可以很容易地得到补救的具有System
出口在哪Subsystem
取决于应用程序的罗伯特*C*马丁 依赖倒置的原则.更好的是将隔离功能Subsystem
s需要从他们的父母,编写一个接口,然后举行一个实施者的界面System
并通过它的Subsystem
s,这将保持它通过一个shared_ptr
.例如,你可能已经LoggerInterface
, ,您的Subsystem
用来写日志,然后你可以得出CoutLogger
或FileLogger
从它和保持的一个实例,在这样System
.
其他提示
这可以正确使用 weak_ptr
类。事实上,你已经非常接近于拥有一个好的解决方案。你是对的,你不能指望“超出思考”。你的客户程序员,你也不应该期望他们总是遵循“规则”。您的API(我相信您已经知道)。所以,你真正做的最好的就是破坏控制。
我建议您对 GetSubsystem
的调用只返回 weak_ptr
而不是 shared_ptr
,以便客户端开发人员可以测试其有效性。指针并不总是声称对它的引用。
同样,让 pParentSystem
成为 boost :: weak_ptr&lt; System&gt;
,以便它可以在内部检测其父 System
是否仍然存在在 pParentSystem
上调用 lock
以及检查 NULL
(原始指针不会告诉你这个)。
假设您更改 Subsystem
类以始终检查其对应的 System
对象是否存在,您可以确保如果客户端程序员尝试使用子系统
对象超出预期范围,导致错误(您控制),而不是一个无法解释的异常(您必须信任客户端程序员捕获/正确处理)。
所以,在你的 main()
的例子中,事情不会是BOOM!在 Subsystem
的dtor中处理此问题的最优雅的方法是让它看起来像这样:
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)
}
...
};
我希望这有帮助!
这里系统显然拥有子系统,我认为共享所有权毫无意义。我只是返回一个原始指针。如果子系统超过其系统,则这本身就是一个错误。
你在第一段开头就是对的。 您的基于RAII的设计(如我的和编写得最好的C ++代码)要求您的对象由独占所有权指针保存。在Boost中将是scoped_ptr。
那你为什么不使用scoped_ptr。这肯定是因为你想要weak_ptr的好处来防止悬空引用,但你只能在weak_ptr指向一个shared_ptr。因此,当您真正想要的是单一所有权时,您采用了通常的方法来方便地声明shared_ptr。这是一个错误的声明,正如你所说,它会破坏以正确顺序调用的析构函数。当然,如果您永远不会分享所有权,那么您将不得不使用它 - 但您必须不断检查所有代码以确保它永远不会被共享。
更糟糕的是,boost :: weak_ptr使用不方便(它没有 - &gt;运算符),因此程序员通过错误地将被动观察引用声明为shared_ptr来避免这种不便。这当然是共享所有权,如果你忘了将该shared_ptr归零,那么当你想要它时,你的对象不会被销毁或者它的析构函数被调用。
简而言之,你已经受到了boost库的影响 - 它无法接受优秀的C ++编程实践,并迫使程序员做出错误的声明,以便尝试从中获得一些好处。它只适用于真正需要共享所有权的脚本胶水代码,并且对以正确顺序调用内存或析构函数的严格控制不感兴趣。
我和你一样走下了同样的道路。在C ++中非常需要防止悬空指针,但是boost库不提供可接受的解决方案。我必须解决这个问题 - 我的软件部门希望保证C ++可以安全。所以我自己动手 - 这是相当多的工作,可以在:
找到http://www.codeproject.com/KB/cpp/XONOR.aspx
它完全适用于单线程工作,我即将更新它以包含跨线程共享的指针。它的关键特征是它支持专有对象的智能(自置零)被动观察器。
不幸的是,程序员已经被垃圾收集和“一刀切”的智能指针解决方案所诱惑,并且在很大程度上甚至没有考虑所有权和被动观察者 - 因此他们甚至不知道他们正在做的是错了,不要抱怨。反对Boost的异端邪说几乎闻所未闻!
向你提出的解决方案非常复杂,根本没有任何帮助。它们是荒谬的例子,这种荒谬是由于文化不愿意认识到对象指针具有必须正确声明的不同角色以及盲目信仰,Boost必须是解决方案。
我没有看到让System :: GetSubsystem将原始指针(RATHER而非智能指针)返回到子系统的问题。由于客户端不负责构造对象,因此客户端没有隐含的合同来负责清理。由于它是一个内部引用,因此假设Subsystem对象的生命周期取决于System对象的生命周期应该是合理的。然后,你应该用文件说明加强这个隐含的合同。
关键是,您没有重新分配或共享所有权 - 那么为什么要使用智能指针?
这里真正的问题是你的设计。没有很好的解决方案,因为该模式不能反映良好设计原则。这是一个方便的法则我的使用:
- 如果一个物体拥有收集的其他目的,并且可以返回的任何任意的对象,收集,然后 删除的对象是从你的设计.
我意识到你的实例是人为的,但其反的模式,我看到了很多工作。问问自己,什么价值 System
加入, std::vector< shared_ptr<SubSystem> >
不?用户API需要知道的界面 SubSystem
(因为你他们返回),所以撰写一持有人为他们只是增加的复杂性。至少有人知道接口 std::vector
, ,迫使他们记住 GetSubsystem()
上 at()
或 operator[]
只是 意思是.
你的问题是有关管理对象的生命周期,但一旦你开始交出了对象,你失去控制的寿命通过允许其他人,让他们活着(shared_ptr
)或风险的崩溃,如果他们使用之后,他们已经走了(原指针).在多线应用其甚至更糟糕的-谁锁的对象,你被派到不同的线?提高公共和弱指针是一个复杂的诱发陷阱,当使用这种方式,特别是因为他们只是线足够安全旅行上缺乏经验的开发。
如果你是要创造一个支架,这需要隐藏的复杂性,从你的用户使用和减轻它们的负担,可以管理自己。作为一个例子,一个接口组成的)发送命令要子系统(例如一URI/系统/子系统/命令?param=value)和b)循环系统和子系统的命令(通过stl状迭代)以及可能的c)寄存器子系统会让你隐藏的,几乎所有的细节你执行你的用户和强制执行辈子/订购/锁定要求内部。
一个iteratable/枚举API是大大优于揭露对象在任何情况下-命令/登记可以很容易地序列化产生的测试案例或配置文件,他们可能显示交互方式(就是说,在一棵树的控制,与对话组成,通过查询可采取的行动/参数)。你也会保护你的API用户从内部改变你可能需要作出系统的课程。
我要提醒你对下面的建议,在阿伦斯答案。设计解决一个问题,这个简单,需要5个不同的设计图案的实施只能意味着错误的问题正在得到解决。我也厌倦的任何人的报价梅耶斯先生关于设计,由于他自己也承认:
"我没有写软件生产中超过20年了,我从来没有写过生产中的软件C++。不,不。此外,我甚至从来没有尝试写制作软件C++,使我不仅不真实的C++开发的,我甚至不是一个崇拜者。平衡这一微的事实是,我做了写研究软件C++在我毕业的学年(1985-1993年任),但即使是小(几千线)的单开发商-是-扔-走-快的东西。并且由于引人注目的作为一个顾问超过十几年前,我的C++编程已经有限的玩具"让我们看看这是怎么运作"(或者,有时,"让我们看看如何,许多编制者使用时就此中断")的程序,通常的程序,适合在一个单一的文件"。
不要说他的书不值得一读,但他无权以英语发言设计或复杂性。
在您的示例中,如果系统持有 vector&lt; Subsystem&gt;
而不是 vector&lt; shared_ptr&lt; Subsystem&gt;,则会更好。 &GT; 代码>。它既简单又消除了您的顾虑。 GetSubsystem会返回一个引用。
堆的对象,将会发布在相反的顺序与它们在那里实例,因此,除非开发人员使用API是试图管理的明智指针,它通常不会是一个问题。有些事你是不可能防止,最好可以做的是提供示警在运行时,优调试只。
你的例子似乎非常喜欢COM对我来说,你有参考数上的子系统正在返回的使用情况,但你错过它在该系统的目的本身。
如果每个子系统的对象做了一个com在该系统的目的在创造和释放在破坏你至少可以显示异常的参数不正确,当该系统的目的是摧毁早。
使用的weak_ptr也会让你提供的消息,而不是/,以及炸毁了当的东西都放在错误的顺序。
您的问题的本质是循环引用:系统是指子系统,而子系统又是指系统。这种数据结构不能通过引用计数轻松处理 - 它需要适当的垃圾收集。你试图通过使用一个边缘的原始指针来打破循环 - 这只会产生更多的复杂性。
至少提出了两个好的解决方案,所以我不会试图超越以前的海报。我只能注意到在@Aaron的解决方案中,你可以拥有系统的代理而不是子系统 - 取决于更复杂和更有意义的东西。