非成员非朋友功能真的会增加封装吗?

时间:2014-10-01 16:04:53

标签: c++

我目前正在阅读Scott Meyers的Effective C ++书籍,但我无法理解第23项。他说:

  

首选非会员非朋友功能到会员功能。这样做可以提高封装,封装灵活性和功能扩展性。

虽然我可以看到在课外添加外部功能的重点,但我看不到它的优点。他谈到这些因为他们正在增加封装。嗯,是的,这是正确的,因为非成员非朋友函数将无法访问在类中声明为私有的任何成员变量。但是,这就是我无法解决的问题。拥有允许对象控制的成员函数在某种程度上是必不可少的 - 对于所有数据成员都是公共的POD可以做些什么?老实说,我看不到任何实际用法。 Havining说,即使我们有非会员非朋友功能,封装也不会因为我们仍然需要而改变!!公众!! MEMBER函数与我们的对象进行交互。

那么为什么我 - 或其他任何人 - 更喜欢非会员非朋友功能而不是会员功能呢?当然,我们可以在现有的成员函数上编写包装器,这些函数可能按逻辑顺序对它们进行分组,但这就是全部。我在这里看不到任何重新封装的封装。

4 个答案:

答案 0 :(得分:9)

迈耶斯在this article中给出了他的推理。这是一个摘录:

  

我们现在已经看到,衡量一个类中封装量的合理方法是计算在类的实现发生变化时可能会破坏的函数数量。在这种情况下,很明显具有n个成员函数的类比具有n + 1个成员函数的类更具封装性。并且这种观察证明了我更倾向于将非成员非友元函数优先于成员函数:如果函数f可以实现为成员函数或非朋友非成员函数,那么使其成为成员会减少封装,而使其成为非成员则不会。

答案 1 :(得分:4)

Meyers 说避免成员函数。他说,功能不应该是的成员(或朋友),除非他们需要。显然需要一些函数可以访问类的私有成员,否则任何其他代码如何与类交互,对吧?

但是,每个可以访问类的私有成员的函数都会耦合到该类的私有实现细节。应该是成员(或朋友)的功能是只能通过访问私人细节才能有效实现的功能。这些是类的原语函数。非原始函数是可以在原始函数之上有效实现的函数。使非原始函数成员(或朋友)增加了与私有细节相关联的代码量。

此外,在编写能够修改对象的私有成员的函数时,必须更加小心以保留类不变量。

答案 2 :(得分:1)

只是一个小例子:

  1. std::list已嵌入sort功能,因为它受益于列表元素的自然移动能力。
  2. 但是如果你无法从内部结构知识中获得任何好处,那就有一个通用的解决方案std::sort

答案 3 :(得分:1)

我将回答OP的问题“为什么有人会更喜欢非会员的非朋友功能而不是会员功能?”用这个简单的例子。考虑一个从地理空间数据生成图形模拟的应用程序。数据以一种表示形式摄取,就像你期望在指南针上看到的那样(以度为单位,顺时针缠绕,其中y轴为0点北/正)。当你将方向信息传递给你的渲染器时,它可能会期望它像你习惯的三角形(弧度,逆时针缠绕,x轴上0点正/正)。

由于方向的两个表示都可以存储为浮点数,因此您可以编写一对盒装基元来强制执行某些类型安全(因此您不会意外地将方位角传递给需要角度的渲染调用)。要在两个表示之间进行转换,可以在Azimuth上编写一个名为AsAngle()的成员函数,并在Angle上编写一个名为AsAzimuth()的成员函数。

class Angle
{
    public:
        float GetValue() const;
        Azimuth AsAzimuth() const;

    private:
        float m_Value;
};

class Azimuth
{
    public:
        float GetValue() const;
        Angle AsAngle() const;

    private:
        float m_Value;
};

这里封装的第一个细分是现在Angle和Azimuth依赖于彼此的类型。你需要在另一个标题中转发声明一个,并在源文件中#include它,以便它可以在转换函数中构造另一个。您可以通过让转换函数返回浮点数而不是其他类的对象来减少此依赖关系,但这并不能完全消除彼此之间的逻辑依赖关系,因为下一个封装细分是两个类都必须知道关于其他

如果您稍后切换到需要以度为单位而不是弧度的角度的渲染器,则可以为此不同的表示更改Angle类。然而,即使唯一的变化是角度的细节,一个完全独立的类,方位角,现在也必须改变,否则它将继续以弧度而不是度数返回角度。如果你更新了Angle的AsAzimuth()成员但是忘记更新Azimuth的AsAngle()成员,你可能最终会看到错误的渲染,当你抓住你的头部时会看到你的更改为Angle时出现错误。< / p>

Azimuth不应该关心Angle的内部细节,但是当你将转换例程实现为成员函数时,它必须这样做。如果您将转换编写为非成员函数,则两个类都不再需要关心另一个类的详细信息 - 如何在两个表示之间进行转换的关注现在完全封装在单独的函数中。

如果您不喜欢在某种实用程序命名空间中使用全局函数或某些随机函数转储的想法,您可以通过创建一个新的Direction类来进一步封装方向的详细信息来改进此设计存储和转换。它可以存储方向,但它来自收集地理空间数据的硬件,比如存储在浮点数中的方位角,并且具有成员函数,可以在类所需的任何表示形式中返回它,仅依赖于视觉提示你做错了什么(比如调用graphicalThingy.SetAngle(direction.AsAzimuth()))。但是如果你不想牺牲盒装基元的类型安全性,你仍然可以使用前两个Angle和Azimuth类,并将转换实现为Direction的一个成员。它仍然是Angle和Azimuth的非成员非朋友功能,它通过使用GetValue()调用的现在较小的公共接口接收它们所需的信息,因此它无法访问任何其他私有成员,它位于一个适当的位置以保持这些功能(Direction类),Angle和Azimuth都不需要关心另一个的细节,也不再相互依赖。

class Direction
{
    public:
        Angle AsAngle() const
        {
            return Angle(Convert(m_Azimuth.GetValue());
        }
        Azimuth AsAzimuth() const
        {
            return m_Azimuth.GetValue();
        }

    private:
        float Convert(const float) const
        {
            ...conversion stuffs here...
        }
        Azimuth m_OriginalAzimuth;
};

在此示例中,转换可以写为成员函数,并且它确实需要来自其使用的类的一段私有数据。但是,绝对没有理由比非成员非友元函数更喜欢成员函数,因为非成员函数改进了封装。