封装集合是一种好方法(正确)吗?
class MyContainedClass { }; class MyClass { public: MyContainedClass * getElement() { // ... std::list<MyContainedClass>::iterator it = ... // retrieve somehow return &(*it); } // other methods private: std::list<MyContainedClass> m_contained; };
尽管msdn说std :: list
不应在删除或者插入时执行元素的重定位,但这是将指针返回到list元素的一种好方法吗?
PS:我知道我可以使用指针集合(并且必须在析构函数中"删除"元素),共享指针集合(我不喜欢)等等。
解决方案
我认为更大的问题是我们要隐藏集合的类型,因此即使我们使用的集合不会移动元素,将来也可能会改变主意。从外部看这是不可见的,所以我想说这不是一个好主意。
我看不到使用这种封装,但这可能只是我一个人。无论如何,返回引用而不是指针对我来说意义更大。
通常,如果"包含类"确实包含在" MyClass"中,则MyClass不应允许外部人员触摸其私有内容。
因此,MyClass应该提供操作所包含的类对象的方法,而不是返回指向它们的指针。因此,例如,使用一种方法,例如"增加第一个包含的对象的值",而不是"这里有一个指向第一个包含的对象的指针,请根据需要对其进行处理"。
这取决于...
这取决于我们希望类封装多少,以及要隐藏或者显示什么。
我看到的代码对我来说似乎还可以。你是对的事实,如果另一个数据/迭代器的修改/删除,std :: list的数据和迭代器不会失效。
现在,返回指针将隐藏我们使用std :: list作为内部容器的事实,并且不会让用户浏览其列表。返回迭代器将为类的用户提供更多的自由来浏览此列表,但他们将"知道"他们正在访问STL容器。
我猜是选择。
请注意,如果它== std :: list <>。end(),则此代码将有问题,但是我想我们已经知道了,这不是本讨论的主题。
不过,我在下面总结了一些替代方案:
使用const
会有所帮助...
我们返回非const指针的事实使对象的用户可以静默修改他/她可以动手使用的任何MyContainedClass,而无需告诉对象。
代替或者返回一个指针,我们可以返回一个const指针(并在方法后缀const),以阻止用户在不使用我们批准的访问器的情况下修改列表中的数据(一种setElement`吗?)。
const MyContainedClass * getElement() const { // ... std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow return &(*it); }
这将在某种程度上增加封装。
那参考呢?
如果方法无法失败(即它始终返回有效的指针),则应考虑返回引用而不是指针。就像是:
const MyContainedClass & getElement() const { // ... std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow return *it; }
不过,这与封装无关。
:-p
使用迭代器?
为什么不返回迭代器而不是指针?如果对我们来说,在列表中上下导航是可以的,则迭代器将比指针更好,并且使用方式大致相同。
如果要避免用户修改数据,请将迭代器设为const_iterator。
std::list<MyContainedClass>::const_iterator getElement() const { // ... std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow return it; }
好的方面是,用户将能够浏览列表。不利的一面是用户会知道这是一个std :: list,所以...
Scott Meyers在他的《有效的STL:提高对标准模板库的使用的50种特定方法》一书中说,仅封装容器是不值得的,因为它们都不可以完全替换为另一个。
当我们从列表中添加或者删除东西时,std :: list不会使任何迭代器,指针或者引用无效(显然,除了要删除该项目的那一点之外),因此以这种方式使用列表不会中断。
正如其他人指出的那样,我们可能不希望将对此类私有位的直接访问权限分发给其他人。因此将函数更改为:
const MyContainedClass * getElement() const { // ... std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow return &(*it); }
可能会更好,或者如果我们始终返回有效的MyContainedClass对象,则可以使用
const MyContainedClass& getElement() const { // ... std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow return *it; }
避免调用代码必须处理NULL指针。
未来的程序员比自定义封装更熟悉STL,因此,如果可以的话,应避免这样做。在应用程序的生命周期中,我们可能会想到一些边缘情况,这些情况会在以后出现,Weras STL经过严格审查和记录。
另外,大多数容器都支持一些类似的操作,例如begin end push等。因此,如果我们要更改容器,则在代码中更改容器类型应该是相当简单的。例如,向量是双端队列或者映射到hash_map等。
假设我们出于更深层次的原因仍要执行此操作,我会说要执行此操作的正确方法是实现列表实现的所有方法和迭代器类。不需要更改时,将呼叫转接到成员列表呼叫。修改并转发或者执行一些需要做一些特殊事情的自定义操作(首先决定这样做的原因)
如果将ST1类设计为从中继承,但是出于效率的考虑,决定不这样做,这将更加容易。 Google的"继承自STL类"的详细信息。
认真思考我们真正想要的MyClass的目的。我注意到,有些程序员只是出于习惯而为包装编写包装器,而不管他们是否有超出标准STL收藏所满足的特定需求。如果情况如此,请typedef std :: list <MyContainedClass> MyClass
并完成此操作。
如果确实有要在" MyClass"中实现的操作,那么封装的成功将更多地取决于为它们提供的接口,而不是如何提供对基础列表的访问。
没有冒犯的意思,但是...由于提供的信息有限,所以闻起来像是在捣鼓:公开内部数据,因为我们无法弄清楚如何实现客户代码在MyClass中所需的操作。 ,或者可能是因为我们甚至还不知道客户端代码将需要执行哪些操作。这是一个经典的问题,试图在需要它的高级代码之前编写低级别的代码。我们知道将要使用的数据,但还没有真正确定要使用的数据,因此我们编写了一个类结构,将原始数据一直公开到顶部。我们最好在这里重新考虑自己的策略。
@cos:
Of course I'm encapsulating MyContainedClass not just for the sake of encapsulation. Let's take more specific example:
示例并没有减轻我担心我们在知道容器将要使用的内容之前就在编写容器的担心。示例容器包装器Document共有三种方法:NewParagraph(),DeleteParagraph()和GetParagraph(),所有这些方法都对包含的集合(std :: list)进行操作,所有这些都紧密地反映了std :: list提供的"开箱即用"的操作。 Document
封装了std :: list,从某种意义上来说,客户端无需了解其在实现中的使用...但是实际上,它只是一个外观,因为我们是在为客户端提供指向存储在列表中的对象的原始指针,客户端仍然隐式绑定到实现。
If we put objects (not pointers) to container they will be destroyed automatically (which is good).
好坏取决于系统的需求。这种实现的意思很简单:文档拥有Paragraph
,并且当从文档中删除Paragraph
时,指向它的任何指针都立即变为无效。这意味着在实现类似以下内容时必须非常小心:
other objects than use collections of paragraphs, but don't own them.
现在你有问题了。对象" ParagraphSelectionDialog"具有指向"文档"所拥有的" Paragraph"对象的指针的列表。如果我们不小心协调这两个对象,则Document或者通过Document的另一个客户端可能会使由ParagraphSelectionDialog实例保存的部分或者全部指针无效!没有简单的方法来捕获指向有效Paragraph
的指针看起来与指向已释放Paragraph
的指针一样,甚至可能指向有效但不同的Paragraph
实例!由于允许甚至期望客户端保留和取消引用这些指针,因此,只要文档从公共方法返回,即使它保留对"对象"对象的所有权,"文档"也会失去对它们的控制。
这不好。我们最终会得到不完整,肤浅的封装,抽象的泄漏,并且在某些方面要比根本没有抽象更糟糕。因为隐藏了实现,所以客户不知道接口所指向的对象的生存期。大多数时候,我们可能会很幸运,因为大多数std :: list
操作不会使对它们未修改项目的引用无效。一切都会好起来的...直到删除了错误的" Paragraph",我们发现自己陷入了在调用堆栈中进行跟踪的任务,寻找使指针停留太长时间的客户端。
修复非常简单:返回值或者对象可以存储的时间长到需要,并在使用前进行验证。这可能很简单,例如必须将序数或者ID值传递给"文档"以换取可用的引用,也可能复杂如引用计数的智能指针或者弱指针……这实际上取决于特定的对象。客户的需求。首先指定客户代码,然后编写"文档"即可使用。
简单的方法
@cos,对于我们所显示的示例,我想说的是用C ++创建此系统的最简单方法是不用担心引用计数。我们要做的就是确保在销毁根Document之前,程序流首先销毁保存了对集合中对象(段落)直接引用的对象(视图)。
艰难的道路
但是,如果我们仍然想通过引用跟踪来控制生存期,则可能必须将引用保持在层次结构的更深处,以使Paragraph对象持有对根Document对象的反向引用,这样,只有当最后一个段落对象被销毁时,Document对象才会被删除。遭到破坏。
另外,段落引用在Views类内部使用并传递给其他类时,也必须作为引用计数接口进行传递。
韧性
与我一开始列出的简单方案相比,这是太多的开销。它避免了各种对象计数的开销,更重要的是,继承我们程序的人员不会陷入纵横交错的系统的参考依赖线程陷阱中。
替代平台
在支持和推广这种编程风格的平台(如.NET或者Java)中,这种工具可能更易于执行。
你还是要担心记忆
即使使用这样的平台,我们仍然必须确保以适当的方式取消引用对象。其他优秀的参考文献可能会在眨眼间吞噬记忆。因此,我们会看到,引用计数并不是良好编程习惯的灵丹妙药,尽管它有助于避免大量错误检查和清除,这在应用整个系统时可大大减轻程序员的工作量。
推荐
就是说,回到原来的问题,该问题引起了所有引用计数疑惑的参考。是否可以直接从集合中公开对象?
如果程序的所有类/所有部分都真正相互依赖,则程序就不存在。不,那是不可能的,因为程序是类/模块交互方式的持续体现。理想的设计只能使依赖性最小化,而不能完全消除依赖性。
因此,我的观点是,是的,如果我们以理性的方式进行操作,则可以将对集合中对象的引用以及需要与它们一起使用的其他对象暴露出来。
- 确保程序中只有少数几个类/部分可以获取此类引用,以确保最小的相互依赖性。
- 确保传递的引用/指针是接口,而不是具体对象,从而避免了具体类之间的相互依赖性。
- 确保没有将引用进一步传递到程序中。
- 在清理满足那些引用的实际对象之前,请确保程序逻辑负责销毁相关对象。