繁体   English   中英

大规模使用Meyer的建议更喜欢非会员,非朋友的功能?

[英]Large scale usage of Meyer's advice to prefer Non-member,non-friend functions?

有一段时间我一直在设计我的类接口是最小的,更喜欢命名空间包装的非成员函数而不是成员函数。 基本上遵循Scott Meyer在非成员函数如何改进封装的文章中的建议。

我在一些小规模的项目中一直在这方面做得很好,但我想知道它在更大规模上的运作情况。 是否有任何大型的,备受推崇的开源C ++项目,我可以看看,也许参考这个建议被强烈遵循的地方?

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

我在我工作的项目上做了很多这样的事情; 在我目前的公司中最大的是大约2M线,但它不是开源的,所以我不能提供它作为参考。 但是,一般来说,我会说我同意这个建议。 您可以越多地将未严格包含的功能与该对象中的一个对象分开,您的设计就越好。

举个例子,考虑一下经典的多态性示例:一个带有子类的Shape基类和一个虚拟的Draw()函数。 在现实世界中,Draw()需要采用一些绘图上下文,并且可能意识到正在绘制的其他内容的状态,或者一般的应用程序。 一旦将所有这些都放入Draw()的每个子类实现中,您可能会有一些代码重叠,或者您的大多数实际Draw()逻辑将在基类中,或者在其他地方。 然后考虑如果你想重用一些代码,你需要在界面中提供更多的入口点,并且可能使用与绘图形状无关的其他代码污染函数(例如:多形图绘制相关逻辑)。 不久之后,它会变得一团糟,你会希望你有一个绘制函数,它取而代之的是Shape(以及上下文和其他数据),而Shape只有完全封装的函数/数据,而不是使用或引用外部物体。

无论如何,这是我的经验/建议,值得的。

我认为随着项目规模的增加,非成员函数的好处也会增加。 标准库容器,迭代器和算法库就是证明。

如果您可以从数据结构中分离算法(或者,以另一种方式表达,如果您可以将对象的操作与内部状态的操作分离),则可以减少类之间的耦合并更好地利用通用代码。

斯科特迈耶斯并不是唯一支持这一原则的作者; Herb Sutter也有,特别是在Monoliths Unstrung中 ,以指南结束:

在可能的情况下,更喜欢将函数写为非成员非朋友。

我认为该文章中一个不必要的成员函数的最好例子之一是std::basic_string::find ; 它没有理由存在,真的,因为std::find提供了完全相同的功能。

OpenCV库就是这样做的。 它们有一个cv :: Mat类,它呈现一个3D矩阵(或图像)。 然后他们拥有cv名称空间中的所有其他函数。

OpenCV库非常庞大,在其领域得到广泛认可。

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

例如,考虑序列容器成员函数insertpush_back 实现push_back至少有两种方法:

  1. 它可以简单地调用insert (它的行为无论如何都是根据insert定义的)
  2. 它可以完成insert所做的所有工作(可能调用私有帮助函数),而无需实际调用insert

显然,在实现序列容器时,您可能希望使用第一种方法。 push_back只是一种特殊形式的insert和(据我所知)你不能通过其他方式实现push_back获得任何性能优势(至少不是listdequevector )。

但是,要彻底测试这样的容器,必须单独测试push_back :因为push_back是一个成员函数,所以它可以修改容器的任何和所有内部状态。 从测试的角度来看,您应该(必须?)假设使用第二种方法实现了push_back因为它可能可以使用第二种方法实现。 无法保证在insert方面实施。

如果将push_back实现为非成员非朋友,则它不能触及容器的任何内部状态; 它必须使用第一种方法。 当您为它编写测试时,您知道它不能破坏容器的内部状态(假设实际的容器成员函数已正确实现)。 您可以使用该知识显着减少为完全执行代码而需要编写的测试数量。

我也做了很多,似乎有意义,它导致完全没有缩放问题。 (虽然我当前的项目只有40000 LOC)事实上,我认为它使代码更具可扩展性 - 它减少了类,减少了依赖性。 它有时需要您重构函数以使它们独立于类的成员 - 从而经常创建一个更通用的辅助函数库,您可以轻松地在其他地方重用它们。 我还要提到许多大项目的常见问题之一是课程膨胀 - 我认为更喜欢非会员,非朋友的功能也有帮助。

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

也就是说,如果您有类模板type<T>

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

和一个可隐式转换为type<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因此将从重载决策集中删除函数foo ,即:未找到任何函数,这意味着不会触发隐式转换。

(我没有时间把它写得很好,以下是一个5分钟的大脑转储,无疑可以在各种各样的试验水平被撕开,但请解决概念和一般推力。)

我对Jonathan Grynspan采取的立场表示了相当的同情,但是想要在评论中合理地做一些事情。

首先 - 向Alf Steinbach“说得好”,他说:“这只是他们观点的过度简化的漫画,似乎可能会发生冲突。对于这个问题,我不同意Scott Meyers在这件事上的意见;我看到他在这里过于概括,或者他是。“

当少数人理解权衡或替代方案时,斯科特,赫伯等人正在提出这些观点,并且他们以不成比例的力量这样做。 人们在代码演变过程中遇到了一些唠叨的麻烦,并且合理地推导出解决这些问题的新设计方法。 让我们回到以后是否存在缺点的问题,但首先 - 值得说的是,所讨论的痛苦通常很小而且很少:非成员函数只是设计可重用代码的一个小方面,而在企业级系统中我是只是编写了你作为非成员编写成员函数的相同类型的代码,很少能够使非成员可重用。 他们甚至表达的算法既复杂又足以值得重用,但却没有严格限制于他们设计的类的特定范围,这很奇怪,实际上不可思议的是,其他一些类将在支持相同的操作和语义。 通常,您还需要模板参数,或引入基类来抽象所需的操作集。 两者都在性能方面具有重要意义,即内联与外联,客户端代码重新编译。

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

但是,作为一个试金石 - 这些非成员函数中有多少与其目前适用的唯一类别位于同一标题中? 有多少人希望通过模板(这意味着内联,编译依赖)或基类(虚函数开销)来抽象他们的参数以允许重用? 两者都不鼓励人们将它们视为可重复使用,但如果不是这样的话,一个类上的可用操作都是非本地化的 ,这会让开发人员对系统的看法受挫:开发经常需要为自己解决一个相当令人失望的事实 - “哦 - 这只适用于X级“。

底线:大多数成员函数不可能重复使用。 许多公司代码没有被分解为干净的算法与可能重用前者的数据。 20年后,这种划分并不是必需的,也不是有用的,也可能是有用的。 它与get / set方法非常相似 - 它们在某些API边界处需要,但在本地化所有权和代码使用时可能构成不必要的冗长。

就个人而言,我没有全部或全部的方法,但根据对潜在的可重用性和界面的位置是否有任何可能的好处,决定制作成员函数或非成员的内容。

如文章所述,STL既有成员也有非成员。 这不是因为他的偏好 - 主要是因为许多自由函数在迭代器上操作而迭代器不在类层次结构中(因为STL希望将数组上的指针作为第一类迭代器)。

我强烈不同意这篇关于C ++的文章,因为不一致会很烦人,而且在现代IDE中,intellisense会被打破。

但是,在C#中,使用扩展方法,这都是非常好的建议。 在那里,您将获得两全其美 - 您可以创建看似成员函数的非成员函数。 它们也可以在单独的文件中 - 获得这种做法的所有好处而没有不一致的缺点。

暂无
暂无

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

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