繁体   English   中英

非成员非友元函数真的增加了封装性吗?

[英]Do non-member non-friend functions really increase encapsulation?

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

比成员函数更喜欢非成员非友元函数。 这样做可以提高封装性、包装灵活性和功能可扩展性。

虽然我可以看到在类之外添加外部函数的意义,但我没有看到它的优势。 他在增加封装时谈论这些。 是的,这是对的,因为非成员非友元函数将无法访问类中声明为私有变量的任何成员变量。 但是,这就是我无法解决的问题。 拥有允许对象控制的成员函数在某种程度上是必不可少的——在所有数据成员都是公共的 POD 上可以做什么? 老实说,我在那里看不到任何实际用途。 哈维宁说过,即使我们有非成员非友元函数,封装也不会改变,因为我们仍然需要!!public!! MEMBER 函数与我们的对象进行交互。

那么为什么我 - 或其他任何人 - 更喜欢非成员非友元函数而不是成员函数? 当然,我们可以为已经存在的成员函数编写包装器,这些成员函数可能按逻辑顺序对它们进行分组,但仅此而已。 我在这里看不到任何简化的封装。

Meyers 在这篇文章中给出了他的推理。 这是一个摘录:

我们现在已经看到,衡量类中封装量的合理方法是计算如果类的实现发生变化可能会被破坏的函数数量。 在这种情况下,很明显具有 n 个成员函数的类比具有 n+1 个成员函数的类封装得更多。 这个观察结果证明了我更喜欢非成员非友函数而不是成员函数的论点:如果函数 f 可以实现为成员函数或非友非成员函数,则使其成为成员会减少封装,而使其成为非会员则不会。

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

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

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

只是一个小例子:

  1. std::list具有sort成员函数,因为它受益于列表元素的自然移动能力。
  2. 但是如果你不能从内部结构知识中获得任何优势,那么有一个通用的解决方案std::sort free function。

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

由于方向的两种表示都可以存储为浮点数,因此您编写了一对装箱基元来强制执行某种类型安全(因此您不会意外地将方位角传递给需要角度的渲染调用)。 要在两种表示之间进行转换,您可以在 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;
};

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

如果您稍后要切换到期望以度为单位的角度而不是弧度的渲染器,您将针对这种不同的表示更改您的 Angle 类。 然而,即使唯一的变化是角度的细节,一个完全独立的类 Azimuth 现在也必须改变,否则它将继续以弧度而不是度数返回角度。 如果您更新了 Angle 的 AsAzimuth() 成员,但忘记更新了 Azimuth 的 AsAngle() 成员,那么当您挠头查看对 Angle 所做的更改时是否有错误时,最终可能会导致渲染看起来错误。

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;
};

在这个例子中,转换可以写成一个成员函数,它确实需要来自它所使用的类的一段私有数据。 然而,绝对没有理由比非成员非友元函数更喜欢成员函数,因为非成员函数改进了封装。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM