Effective C++ by Scott Meyers在第5章第28项中告诉我们避免将“句柄”(指针,引用或迭代器)返回给对象内部,这绝对是一个好点。
即。不要这样做:
class Family
{
public:
Mother& GetMother() const;
}
因为它破坏了封装并允许改变私有对象成员。
甚至不要这样做:
class Family
{
public:
const Mother& GetMother() const;
}
因为它可能会导致“悬空手柄”,这意味着您保留对已经销毁的对象成员的引用。
现在,我的问题是,有什么好的选择吗?想象一下妈妈很沉重!如果我现在返回母亲的副本而不是参考,GetMother正在成为一项相当昂贵的操作。
你如何处理这类案件?
答案 0 :(得分:14)
首先,让我重新进行迭代:最大的问题不是生命周期,而是封装之一。
封装不仅意味着没有人能够在没有意识到内容的情况下修改内部,封装意味着没有人知道你的类中是如何实现的,所以你可以随意改变类内部,只要你保持界面完全相同。
现在,您返回的引用是否为const
并不重要:您不小心暴露了Mother
类内部有Family
个对象的事实,现在您只是无法摆脱它(即使你有更好的代表性),因为你的所有客户都可能依赖它并且必须改变他们的代码......
最简单的解决方案是按值返回:
class Family {
public:
Mother mother() { return _mother; }
void mother(Mother m) { _mother = m; }
private:
Mother _mother;
};
因为在下一次迭代中我可以在不破坏界面的情况下删除_mother
:
class Family {
public:
Mother mother() { return Mother(_motherName, _motherBirthDate); }
void mother(Mother m) {
_motherName = m.name();
_motherBirthDate = m.birthDate();
}
private:
Name _motherName;
BirthDate _motherBirthDate;
};
看看我是如何在不改变iota接口的情况下完全改造内部构件的?容易Peasy。
注意:显然这种转换只是为了效果......
显然,这种封装是以某种性能为代价的,这里有一种紧张,每次你写一个吸气剂时,你的判断是否应该首选封装或性能。
答案 1 :(得分:5)
可能的解决方案取决于您的课程的实际设计以及您认为“对象内部”的内容。
Mother
只是Family
的实施细节,可以完全隐藏Family
用户Family
被视为其他公共对象的组合在第一种情况下,您应完全封装子对象并仅通过Family
函数成员(可能重复Mother
公共接口)提供对它的访问:
class Family
{
std::string GetMotherName() const { return mommy.GetName(); }
unsigned GetMotherAge() const { return mommy.GetAge(); }
...
private:
Mother mommy;
...
};
嗯,如果Mother
接口非常大,可能会很无聊,但这可能是设计问题(良好的接口应该有3-5-7个成员),这会让你重新审视并重新设计一些更好的接口方式。
在第二种情况下,您仍然需要返回整个对象。有两个问题:
Mother
定义)要解决问题1使用界面而不是特定类,要解决问题2使用共享或弱所有权:
class IMother
{
virtual std::string GetName() const = 0;
...
};
class Mother: public IMother
{
// Implementation of IMother and other stuff
...
};
class Family
{
std::shared_ptr<IMother> GetMother() const { return mommy; }
std::weak_ptr<IMother> GetMotherWeakPtr() const { return mommy; }
...
private:
std::shared_ptr<Mother> mommy;
...
};
答案 2 :(得分:4)
如果您正在阅读只读视图,并且由于某种原因您需要避免悬空手柄,那么您可以考虑返回shared_ptr<const Mother>
。
这样,Mother
对象可以超出Family
对象。当然,这也必须由shared_ptr
存储。
部分考虑因素是你是否要通过使用太多shared_ptr
来创建引用循环。如果你是,那么你可以考虑weak_ptr
,你也可以考虑接受悬空句柄的可能性,但是编写客户端代码以避免它。例如,没有人担心std::vector::at
返回一个在向量被销毁时变得陈旧的引用这一事实。但是,容器是一个有意暴露它“拥有”的对象的类的极端例子。
答案 3 :(得分:3)
这可以追溯到一个基本的OO原则:
Tell objects what to do rather than doing it for them.
您需要Mother
做一些有用的事情吗?请Family
对象为您执行此操作。通过Class
对象上方法的参数,将它包含在一个漂亮的接口(c ++中的Family
)中。
答案 4 :(得分:2)
因为它可以导致“悬挂手柄”,这意味着你保持一个 引用已经销毁的对象的成员。
您的用户也可以取消引用null
或同样愚蠢的东西,但他们不会这样做,只要生命周期清晰且定义明确,他们也不会这样做。这没什么不对。
答案 5 :(得分:0)
这只是一个语义问题。在您的情况下,Mother
不是 Family
内部,而不是其实现细节。 Mother
类实例可以在Family
以及许多其他实体中引用。此外,Mother
实例生命周期甚至可能与Family
生命周期无关。
因此,更好的设计将存储在Family
shared_ptr<Mother>
中,并在Family
界面中公开,无需担心。