代码之家  ›  专栏  ›  技术社区  ›  ergosys

大规模使用迈耶的建议,更喜欢非成员,非朋友的功能?

  •  41
  • ergosys  · 技术社区  · 14 年前

    一段时间以来,我一直将类接口设计为最小的接口,与成员函数相比,我更喜欢命名空间包装的非成员函数。基本上遵循斯科特·迈耶在文章中的建议 How Non-Member Functions Improve Encapsulation .

    我已经在一些小规模的项目中取得了很好的效果,但我想知道它在更大规模上的效果如何。有没有什么大的、被广泛看好的开源C++项目,我可以看看,也许可以参考一下,强烈建议这个建议?

    更新:感谢所有的投入,但我不是真的对意见感兴趣,而是想知道它在更大规模的实践中有多好。尼克的答案在这方面是最接近的,但我希望能够看到代码。任何形式的实践经验的详细描述(积极的,消极的,实际的考虑,等等)都是可以接受的。

    8 回复  |  直到 14 年前
        1
  •  5
  •   Dat Chu    14 年前

    OpenCV库可以做到这一点。它们有一个cv::Mat类,它表示一个3D矩阵(或图像)。然后它们拥有cv命名空间中的所有其他函数。

        2
  •  11
  •   Nick    14 年前

    我在我工作的项目中经常这样做;在我目前的公司,最大的一个项目大约有200万行,但它不是开源的,所以我不能提供它作为参考。不过,总的来说,我同意这个建议。您可以将不严格包含在一个对象中的功能与该对象分离得越多,您的设计就越好。

    举个例子,考虑一下经典的多态性示例:一个带有子类的Shape基类和一个virtual Draw()函数。在现实世界中,Draw()需要获取一些绘图上下文,并且可能知道正在绘制的其他事物的状态,或者应用程序的一般状态。一旦您将所有这些放到Draw()的每个子类实现中,您可能会有一些代码重叠,或者您的大多数实际Draw()逻辑将位于基类或其他地方。然后考虑一下,如果您想重用其中的一些代码,您需要向接口提供更多的入口点,并且可能会用其他与绘制形状无关的代码(例如:多形状绘制关联逻辑)污染函数。不久,它将是一个混乱,你会希望你有一个绘图函数,它采取了一个形状(和上下文,和其他数据),而形状只是有函数/数据,完全封装,不使用或引用外部对象。

    不管怎样,这是我的经验/建议。

        3
  •  9
  •   James McNellis    14 年前

    如果可以将算法与数据结构解耦(或者,换一种说法,如果可以将处理对象的操作与处理对象内部状态的方式解耦),则可以减少类之间的耦合,并更好地利用泛型代码。

    Monoliths Unstrung ,以指南结尾:

    我认为这篇文章中关于一个不必要的成员函数的最好例子是 std::basic_string::find 它没有存在的理由,真的,就像 std::find 提供完全相同的功能。

        4
  •  5
  •   James McNellis    14 年前

    作为非成员非朋友编写函数的一个实际优势是这样做可以显著减少彻底测试和验证代码所需的时间。

    例如,考虑序列容器成员函数 insert push_back . 至少有两种实现方法 向后推

    1. 它可以简单地调用 (它的行为是根据 插入 (无论如何)
    2. 插入 插入

    显然,在实现序列容器时,您可能希望使用第一种方法。 向后推 只是一种特殊的 插入 而且(据我所知),通过实施 向后推 另一种方式(至少不是 list deque ,或 vector

    但是,要彻底测试这样的容器,您必须测试 向后推 向后推 是一个成员函数,它可以修改容器的任何和所有内部状态。从测试的角度来看,你应该(必须?)假设 向后推 是使用第二种方法实现的,因为它可能使用第二种方法实现。不能保证它在以下方面得到实施: 插入 .

    向后推 作为非成员非朋友实现,它不能触及容器的任何内部状态;它必须使用第一种方法。当您为它编写测试时,您知道它不能破坏容器的内部状态(假设实际的容器成员函数实现正确)。您可以使用这些知识来显著减少为充分使用代码而需要编写的测试的数量。

        5
  •  2
  •   Tony Delroy    14 年前

    我对乔纳森·格林斯潘所采取的立场深表同情,但我想说的比评论中所能说的要多一些。

    斯科特、赫伯等人在提出这些观点时,很少有人理解权衡或替代方案,而且他们的理解力不成比例。分析了人们在代码演化过程中遇到的一些麻烦,提出了一种新的设计方法

    也就是说,如果操作是按照公共接口实现的,那么在更改实现时通常需要较少的代码更改和影响研究,并且作为非友人非成员系统地执行这些操作。但有时,它会使初始实现更加冗长,或者在其他方面不那么理想和可维护。

    但是,作为一个试金石测试,这些非成员函数中有多少与它们当前唯一适用的类位于同一个头文件中?有多少人希望通过模板(即内联、编译依赖项)或基类(虚拟函数开销)来抽象参数以允许重用?两者都不鼓励人们将它们视为可重用的,但如果不是这样,则类上可用的操作是可重用的 离域的

    一句话:大多数成员函数都是不可重用的。许多公司代码没有被分解成干净的算法和数据,前者有可能被重用。这种划分在未来的20年里是不必要的,也没有用的,或者可以想象是有用的。它与get/set方法非常相似-它们在某些API边界上是必需的,但是当代码的所有权和使用被本地化时,它们可能会构成不必要的冗长。

    就我个人而言,我并没有一个全有或全无的方法来解决这个问题,但是根据是否有任何可能的好处,潜在的可重用性和接口的局部性,来决定什么是成员函数还是非成员函数。

        6
  •  2
  •   darklon    14 年前

    我也经常这样做,这似乎是有意义的,它绝对不会导致缩放问题。(尽管我目前的项目只有40000个LOC)事实上,我认为它使代码更具可伸缩性——它精简了类,减少了依赖性。 有时需要重构函数,使其独立于类的成员,从而常常创建一个更通用的助手函数库,您可以在其他地方轻松地重用这些函数。我还提到,许多大型项目的一个常见问题是类的膨胀——我认为更喜欢非成员、非友元函数也有帮助。

        7
  •  1
  •   gnzlbg    9 年前

    首选非成员非友元函数进行封装 除非 您希望隐式转换适用于类模板非成员函数(在这种情况下,最好使它们成为朋友函数):

    type<T> :

    template<class T>
    struct type {
      void friend foo(type<T> a) {}
    };
    

    以及隐式转换为的类型 类型<T>

    template<class T>
    struct convertible_to_type {
      operator type<T>() { }
    };
    

    auto t = convertible_to_type<int>{};
    foo(t);  // t is converted to type<int>
    

    但是,如果你 foo 非朋友 功能:

    template<class T>
    void foo(type<T> a) {}
    

    auto t = convertible_to_type<int>{};
    foo(t);  // FAILS: cannot deduce type T for type
    

    T 然后是函数 从重载解析集中删除,即:找不到函数,这意味着隐式转换不会触发。