有一段时间以来,我一直在设计我的类接口是最小的,更喜欢使用命名空间包装的非成员函数而不是成员函数。基本上遵循Scott Meyer在文章How Non-Member Functions Improve Encapsulation中的建议。
我在一些小规模的项目中做得很好,但是我想知道它在更大规模上的运作情况。是否有任何大型的,备受推崇的开源C ++项目,我可以看看,也许参考这个建议被强烈遵循的地方?
更新:感谢所有的意见,但我并不是真的对意见感兴趣,而是在大规模的实践中找出它的效果。尼克的答案在这方面最接近,但我希望能够看到代码。任何形式的实践经验的详细描述(积极,消极,实际考虑等)也是可以接受的。
答案 0 :(得分:11)
我在我工作的项目上做了很多这样的事情;在我目前的公司中最大的是大约2M线,但它不是开源的,所以我不能提供它作为参考。但是,一般来说,我会说我同意这个建议。您可以越多地将未严格包含的功能与该对象中的一个对象分开,您的设计就越好。
举个例子,考虑经典的多态性示例:一个带有子类的Shape基类和一个虚拟的Draw()函数。在现实世界中,Draw()需要采用一些绘图上下文,并且可能知道正在绘制的其他内容的状态,或者一般的应用程序。一旦将所有这些都放入Draw()的每个子类实现中,您可能会有一些代码重叠,或者您的大多数实际Draw()逻辑将在基类中,或者在其他地方。然后考虑如果你想重用一些代码,你需要在界面中提供更多的入口点,并且可能使用与绘图形状无关的其他代码污染函数(例如:多形状绘图相关逻辑)。不久之后,它会变得一团糟,你会希望你有一个绘制函数,它取而代之的是Shape(以及上下文和其他数据),而Shape只有完全封装的函数/数据,而不是使用或引用外部对象。
无论如何,这是我的经验/建议,值得的。
答案 1 :(得分:9)
我认为随着项目规模的增加,非会员职能的好处也会增加。标准库容器,迭代器和算法库证明了这一点。
如果您可以将算法与数据结构分离(或者,以另一种方式表达,如果您可以将对象的操作与内部状态的操作分离),则可以减少类之间的耦合并更好地利用通用代码。
斯科特迈耶斯并不是唯一支持这一原则的作者; Herb Sutter也有,特别是在Monoliths Unstrung中,以指南结尾:在可能的情况下,更喜欢将函数写为非成员非朋友。
我认为该文章中一个不必要的成员函数的最佳例子之一是std::basic_string::find
;实际上没有理由存在,因为std::find
提供了完全相同的功能。
答案 2 :(得分:5)
OpenCV库就是这样做的。它们有一个cv :: Mat类,它呈现一个3D矩阵(或图像)。然后他们拥有cv名称空间中的所有其他函数。
OpenCV库非常庞大,并且在其领域得到广泛认可。
答案 3 :(得分:4)
将函数编写为非成员非朋友的一个实际优点是,这样做可以显着减少彻底测试和验证代码所需的时间。
例如,考虑序列容器成员函数insert
和push_back
。实施push_back
至少有两种方法:
insert
(无论如何,它的行为是以insert
定义的)insert
所做的所有工作(可能调用私有帮助函数)而不实际调用insert
显然,在实现序列容器时,您可能希望使用第一种方法。 push_back
只是insert
的一种特殊形式,并且(据我所知),通过其他方式实施push_back
,您无法获得任何性能优势(至少不是{ {1}},list
或deque
)。
但是,要彻底测试此类容器,您必须单独测试vector
:由于push_back
是成员函数,因此它可以修改容器的任何和所有内部状态。从测试的角度来看,您应该(必须?)假设push_back
是使用第二种方法实现的,因为它可能可以使用第二种方法实现。无法保证以push_back
。
如果insert
被实现为非成员非朋友,则它无法触及容器的任何内部状态;它必须使用第一种方法。当您为它编写测试时,您知道它不能破坏容器的内部状态(假设实际的容器成员函数已正确实现)。您可以使用该知识显着减少为完全运用代码而需要编写的测试数量。
答案 4 :(得分:2)
我也做了很多,似乎有意义,并且它完全没有缩放问题。 (虽然我当前的项目只有40000 LOC)事实上,我认为它使代码更具可扩展性 - 它减少了类,减少了依赖性。 它有时需要您重构函数以使它们独立于类的成员 - 从而经常创建一个更通用的辅助函数库,您可以轻松地在其他地方重用它们。我还要提到许多大项目的常见问题之一是课程膨胀 - 我认为更喜欢非会员,非朋友的功能也有帮助。
答案 5 :(得分:1)
(我没有时间把它写得很好,以下是一个5分钟的大脑转储,毫无疑问可以在不同的三等级水平被撕开,但请解决概念和一般推力。)
我对Jonathan Grynspan所采取的立场表示了相当的同情,但是想要多说一些,而不是在评论中合理地做出来。
首先 - 对Alf Steinbach表示“好好说”,他说:“这只是他们观点的过度简化的漫画,似乎可能会发生冲突。对于它的价值,我不同意Scott Meyers的观点。事情;我认为他在这里过于概括,或者他是。“
当很少有人理解权衡或替代方案时,斯科特,赫伯等人正在提出这些观点,并且他们以不成比例的力量这样做。人们在代码演变过程中遇到了一些麻烦的麻烦,并且合理地推导出一种解决这些问题的新设计方法。让我们回到以后是否存在缺点的问题,但首先 - 值得说的是,所讨论的痛苦通常很小而且很少:非成员函数只是设计可重用代码的一个小方面,而在企业级系统中我是只是编写了你作为非成员编写成员函数的相同类型的代码,很少能够使非成员可重用。他们甚至表达的算法既复杂又足以值得重用,但却没有严格限制于他们设计的类的特定范围,这很奇怪,实际上不可思议的是,其他一些类将在支持相同的操作和语义。通常,您还需要模板参数,或引入基类来抽象所需的操作集。两者都在性能方面具有重要意义,即内联与外线,客户端代码重新编译。也就是说,如果操作已经在公共接口方面实现,那么在更改实现时通常需要更少的代码更改和影响研究,并且作为非朋友的非成员系统地强制执行。但有时候,它会使初始实现更加冗长,或者以某种其他方式使其不太理想和可维护。
但是,作为一个试金石 - 这些非成员函数中有多少与其目前适用的唯一类别位于同一标题中?有多少人希望通过模板(这意味着内联,编译依赖)或基类(虚函数开销)来抽象他们的参数以允许重用?两者都不鼓励人们将它们看作是可重用的,但如果不是这样的话,一个类上的可用操作就会 delocalised ,这会让开发人员对系统的看法受挫:开发经常需要自己解决问题。相当令人失望的事实 - “哦 - 这只适用于X级”。
底线:大多数成员函数不可能重复使用。许多公司代码没有被分解为干净的算法与可能重用前者的数据。 20年后,这种划分并不是必需的,也不是有用的,也可能是有用的。它与get / set方法非常相似 - 它们在某些API边界处需要,但在代码的所有权和使用本地化时可能构成不必要的详细程度。
就个人而言,我没有全部或全部的方法,但根据是否有任何可能的好处,潜在的可重用性与接口的位置,决定制作成员函数或非成员的内容。
答案 6 :(得分:1)
首选非成员非朋友函数进行封装 除非您希望隐式转换适用于类模板非成员函数(在这种情况下,您最好让他们成为朋友函数):
也就是说,如果你有一个类模板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
,即:找不到函数,这意味着隐式转换不会触发。
答案 7 :(得分:-1)
如文章所述,STL既有成员也有非成员。这不是因为他的偏好 - 主要是因为许多自由函数在迭代器上运行而迭代器不在类层次结构中(因为STL希望将数组上的指针作为第一类迭代器)。
我强烈不同意这篇关于C ++的文章,因为这种不一致会很烦人,而且在现代IDE中,智能感知会被打破。
然而,在C#中,使用扩展方法,这都是非常好的建议。在那里,您将获得两全其美 - 您可以创建看似成员函数的非成员函数。它们也可以在单独的文件中 - 获得这种做法的所有好处,而没有不一致的缺点。