大规模使用迈耶的建议来选择非会员、非好友功能?
一段时间以来,我一直将类接口设计得最小化,与成员函数相比,我更喜欢命名空间包装的非成员函数。基本上遵循 Scott Meyer 在文章非成员函数如何改进封装中的建议。
我在一些小规模项目中这样做并取得了良好的效果,但我想知道它在更大范围内的效果如何。是否有任何大型的、备受推崇的开源 C++ 项目可供我查看,或许可以参考一下强烈遵循此建议的地方?
更新:感谢您提供的所有意见,但我对意见并不真正感兴趣,而是想知道它在更大范围内的实践中效果如何。尼克的答案在这方面最接近,但我希望能够看到代码。对实际经验(积极的、消极的、实际考虑等)的任何形式的详细描述也是可以接受的。
For some time I've been designing my class interfaces to be minimal, preferring namespace-wrapped non-member functions over member functions. Essentially following Scott Meyer's advice in the article How Non-Member Functions Improve Encapsulation.
I've been doing this with good effect in a few small scale projects, but I'm wondering how well it works on a larger scale. Are there any large, well regarded open-source C++ projects that I can take a look at and perhaps reference where this advice is strongly followed?
Update: Thanks for all the input, but I'm not really interested in opinion so much as finding out how well it works in practice on a larger scale. Nick's answer is closest in this regard, but I'd like to be able to see the code. Any sort of detailed description of practical experiences (positives, negatives, practical considerations, etc) would be acceptable as well.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
优先选择非成员非友元函数进行封装 除非您希望隐式转换适用于类模板非成员函数(在这种情况下,您最好将它们设为友元函数):
也就是说,如果您有一个类模板
type
:和一个可隐式转换为
type
的类型,例如:以下内容按预期工作:
但是,如果您将
foo
设为非友元函数:那么以下内容不起作用:
由于您无法推导出
T
,则函数foo
从重载决议集中移除,即:没有找到函数,这意味着隐式转换不会触发。Prefer non-member non-friend functions for encapsulation UNLESS you want implicit conversions to work for class templates non-member functions (in which case you better make them friend functions):
That is, if you have a class template
type<T>
:and a type implicitly convertible to
type<T>
, e.g.:The following works as expected:
However, if you make
foo
a non-friend function:then the following doesn't work:
Since you cannot deduce
T
then the functionfoo
is removed from the overload resolution set, that is: no function is found, which means that the implicit conversion does not trigger.我在我从事的项目中经常这样做;我现在公司最大的有2M行左右,但不是开源的,所以无法提供参考。不过,总的来说,我会说我同意这个建议。您将不严格包含在一个对象中的功能与该对象分离得越多,您的设计就会越好。
举例来说,考虑经典的多态性示例:具有子类的 Shape 基类和虚拟 Draw() 函数。在现实世界中,Draw() 需要获取一些绘图上下文,并可能了解正在绘制的其他事物或一般应用程序的状态。一旦将所有这些放入 Draw() 的每个子类实现中,您可能会出现一些代码重叠,或者大部分实际的 Draw() 逻辑将位于基类中或其他地方。然后考虑一下,如果您想重用其中一些代码,则需要提供更多的接口入口点,并且可能会用与绘制形状无关的其他代码污染函数(例如:多形状绘制相关逻辑) )。不久之后,它就会变得一团糟,你会希望有一个绘制函数,它采用 Shape(以及上下文和其他数据),而 Shape 只是具有完全封装的函数/数据,而不使用或引用外部物体。
无论如何,这就是我的经验/建议,无论其价值如何。
I do this quite a bit on the project I work on; the largest of which at my current company is around 2M lines, but it's not open source, so I can't provide it as a reference. However, I will say that I agree with the advice, generally speaking. The more you can separate the functionality which is not strictly contained to just one object from that object, the better your design will be.
By way of an example, consider the classic polymorphism example: a Shape base class with subclasses, and a virtual Draw() function. In the real world, Draw() would need to take some drawing context, and potentially be aware of the state of other things being drawn, or the application in general. Once you put all that into each subclass implementation of Draw(), you're likely to have some code overlap, or most of your actual Draw() logic will be in the base class, or somewhere else. Then consider that if you want to re-use some of that code, you'll need to provide more entry points into the interface, and possibly pollute the functions with other code not related to drawing shapes (eg: multi-shape drawing correlation logic). Before long, it'll be a mess, and you'll wish you had a draw function which took a Shape (and context, and other data) instead, and Shape just had functions/data which were entirely encapsulated and not using or referencing external objects.
Anyway, that's my experience/advice, for what it's worth.
我认为,随着项目规模的增加,非成员职能的好处也会增加。标准库容器、迭代器和算法库证明了这一点。
如果您可以将算法与数据结构解耦(或者,换句话说,如果您可以将对象的操作与其内部状态的操作方式解耦),则可以减少类之间的耦合并更好地利用通用代码。
斯科特·迈耶斯并不是唯一支持这一原则的作者。 Herb Sutter 也有,特别是在 Monoliths Unstrung 中,结尾与指导方针:
我认为该文章中不必要的成员函数的最佳示例之一是
std::basic_string::find
;实际上,它没有理由存在,因为 std::find 提供了完全相同的功能。I'd argue that the benefit of non-member functions increases as the size of the project increases. The standard library containers, iterators, and algorithms library are proof of this.
If you can decouple algorithms from data structures (or, to phrase it another way, if you can decouple what you do with objects from how their internal state is manipulated), you can decrease coupling between your classes and take greater advantage of generic code.
Scott Meyers isn't the only author who has argued in favor of this principle; Herb Sutter has too, especially in Monoliths Unstrung, which ends with the guideline:
I think one of the best examples of an unneccessary member function from that article is
std::basic_string::find
; there is no reason for it to exist, really, asstd::find
provides exactly the same functionality.OpenCV 库就是这样做的。他们有一个 cv::Mat 类来呈现 3D 矩阵(或图像)。然后他们拥有 cv 命名空间中的所有其他函数。
OpenCV 库非常庞大,并且在其领域受到广泛重视。
OpenCV library does this. They have a cv::Mat class that presents a 3D matrix (or images). Then they have all the other functions in the cv namespace.
OpenCV library is huge and is widely regarded in its field.
以非成员非友元形式编写函数的一个实际优点是,这样做可以显着减少彻底测试和验证代码所需的时间。
例如,考虑序列容器成员函数
insert
和push_back
。至少有两种方法来实现push_back
:insert
(无论如何,它的行为是根据insert
定义的)insert
会执行的所有工作(可能调用私有辅助函数),而无需实际调用insert
显然,在实现序列容器时,您可能希望使用第一种方法。
push_back
只是insert
的一种特殊形式,并且(据我所知)您无法通过实现push_back
真正获得任何性能优势其他方式(至少不适用于list
、deque
或vector
)。但是,要彻底测试这样的容器,您必须单独测试
push_back
:由于push_back
是一个成员函数,它可以修改容器的任何和所有内部状态。从测试的角度来看,您应该(必须?)假设push_back
是使用第二种方法实现的,因为它有可能使用第二种方法实现。不保证它是按照insert
实现的。如果
push_back
实现为非成员非友元,则它无法触及容器的任何内部状态;它必须使用第一种方法。当您为它编写测试时,您知道它不能破坏容器的内部状态(假设实际的容器成员函数已正确实现)。您可以利用这些知识来显着减少充分执行代码所需编写的测试数量。One practical advantage of writing functions as nonmember nonfriends is that doing so can significantly reduce the time it takes to thoroughly test and verify the code.
Consider, for example, the sequence container member functions
insert
andpush_back
. There are at least two approaches to implementingpush_back
:insert
(it's behavior is defined in terms ofinsert
anyway)insert
would do (possibly calling private helper functions) without actually callinginsert
Obviously, when implementing a sequence container, you probably want to use the first approach.
push_back
is just a special form ofinsert
and (to the best of my knowledge) you can't really get any performance benefit by implementingpush_back
some other way (at least not forlist
,deque
, orvector
).However, to thoroughly test such a container, you have to test
push_back
separately: sincepush_back
is a member function, it can modify any and all of the internal state of the container. From a testing standpoint, you should (must?) assume thatpush_back
is implemented using the second approach because it is possible that it could be implemented using the second approach. There is no guarantee that it is implemented in terms ofinsert
.If
push_back
is implemented as a nonmember nonfriend, it can't touch any of the internal state of the container; it must use the first approach. When you write tests for it, you know that it can't break the internal state of the container (assuming the actual container member functions are implemented correctly). You can use that knowledge to significantly reduce the number of tests that you need to write to fully exercise the code.(我没有时间把这个写得很好,下面是一个 5 分钟的大脑转储,毫无疑问可以在各种琐碎的层面上进行分解,但请说明概念和总体主旨。)
我对乔纳森·格林斯潘(Jonathan Grynspan),但想对此多说一点,而无法在评论中合理地做到这一点。
首先——对阿尔夫·斯坦巴赫说得好,他插话道:“这只是对他们观点的过度简化的讽刺,可能看起来是相互冲突的。无论如何,我在这个问题上不同意斯科特·迈耶斯的观点;因为我认为他在这里过于概括了,或者说他确实是这样。”
斯科特、赫伯等人在提出这些观点时,很少有人理解其中的权衡或替代方案,而且他们的力度不成比例。分析了人们在代码演化过程中遇到的一些令人烦恼的麻烦,并合理地导出了解决这些问题的新设计方法。让我们回到稍后是否有缺点的问题,但首先 - 值得一提的是,所讨论的痛苦通常很小且不常见:非成员函数只是设计可重用代码的一个小方面,并且在企业级系统中我已经仅仅编写与非成员一样放入成员函数中的相同类型的代码很少足以使非成员可重用。对他们来说,表达既复杂到值得重用,又不与它们设计的特定类紧密结合的算法是相当罕见的,这很奇怪,以至于几乎无法想象其他类会在支持相同的操作和语义。通常,您还需要模板参数,或引入基类来抽象所需的操作集。两者对性能、内联与外联、客户端代码重新编译都有重大影响。
也就是说,如果操作是根据公共接口实现的,并且作为非朋友非成员系统地强制执行,则在更改实现时所需的代码更改和影响研究通常会较少。但有时,它会使最初的实现变得更加冗长,或者以其他方式变得不太理想和可维护。
但是,作为试金石 - 这些非成员函数中有多少与它们当前适用的唯一类位于同一标头中?有多少人希望通过模板(这意味着内联、编译依赖项)或基类(虚拟函数开销)抽象其参数以允许重用?两者都阻止人们将它们视为可重用的,但如果情况并非如此,类上可用的操作非本地化,这可能会挫伤开发人员对系统的看法:开发人员通常必须自己解决问题相当令人失望的事实是——“哦——这只适用于 X 类”。
底线:大多数成员函数都不可重用。许多公司代码没有被分解为干净的算法和有可能重用前者的数据。这种划分在未来 20 年是没有必要的,也没有用处,也没有想象中有用。它与 get/set 方法非常相似 - 在某些 API 边界上需要它们,但当代码的所有权和使用本地化时,它们可能会造成不必要的冗长。
就我个人而言,我对此没有采取全有或全无的方法,而是根据潜在的可重用性与接口的局部性是否有任何可能的好处来决定将什么设为成员函数或非成员函数。
(I don't have time to write this up nicely, the following's a 5 minute brain dump which doubtless can be ripped apart at various trival levels, but please address the concepts and general thrust.)
I have considerable sympathy for the position taken by Jonathan Grynspan, but want to say a bit more about it than can reasonably be done in comments.
First - a "well said" to Alf Steinbach, who chipped in with "It's only over-simplified caricatures of their viewpoints that might seem to be in conflict. For what it's worth I don't agree with Scott Meyers on this matter; as I see it he's over-generalizing here, or he was."
Scott, Herb etc. were making these points when few people understood the trade-offs or alternatives, and they did so with disproportionate strength. Some nagging hassles people had during evolution of code were analysed and a new design approach addressing those issues was rationally derived. Let's return to the question of whether there were downsides later, but first - worth saying that the pain in question was typically small and infrequent: non-member functions are just one small aspect of designing reusable code, and in enterprise scale systems I've worked on simply writing the same kind of code you'd have put into a member function as a non-member is rarely enough to make the non-members reusable. It's pretty rare for them to even express algorithms that are both complex enough to be worth reusing and yet not tightly bound to the specific of the class they were designed for, that being weird enough that it's practically inconceivable some other class will happen along supporting the same operations and semantics. Often, you also need to template arguments, or introduce a base class to abstract the set of operations required. Both have significant implications in terms of performance, being inline vs out-of-line, client-code recompilation.
That said, there's often less code changes and impact study required when changing implementation if operations have been implementing in terms of a public interface, and being a non-friend non-member systematically enforces that. Occasionally though, it makes the initial implementation more verbose or in some other way less desirable and maintainble.
But, as a litmus test - how many of these non-member functions sit in the same header as the only class for which they're currently applicable? How many want to abstract their arguments via templates (which means inlining, compilation dependencies) or base classes (virtual function overheads) to allow reuse? Both discourage people from seeing them as reusable, but when not the case, the operations available on a class are delocalised, which can frustrate developers perception of a system: the develop often has to work out for themselves the rather disappointing fact that - "oh - that will only work for class X".
Bottom line: most member functions aren't potentially reusable. Much corporate code isn't broken into clean algorithm versus data with potential for reuse of the former. That kind of division just isn't required or useful or conceivably useful 20 years down the road. It's much the same as get/set methods - they're needed at certain API boundaries, but can constitute needless verbosity when ownership and use of the code is localised.
Personally, I don't have an all or nothing approach to this, but decide what to make a member function or non-member based on whether there's any likely benefit to either, potential reusability versus locality of interface.
我也经常这样做,这似乎很有意义,而且它绝对不会导致缩放问题。 (虽然我当前的项目只有 40000 LOC)事实上,我认为它使代码更具可扩展性 - 它精简了类,减少了依赖性。
有时,它需要您重构您的函数,使它们独立于类的成员 - 从而通常创建一个更通用的辅助函数库,您可以在其他地方轻松地重用它。我还要提到,许多大型项目的常见问题之一是类的膨胀 - 我认为更喜欢非成员、非友元函数在这里也有帮助。
I also do this alot, where it seems to make sense, and it causes absolutely no problems with scaling. (although my current project is only 40000 LOC) In fact, I think it makes the code more scalable - it slims down classes, reduces dependencies.
It sometimes requires you to refactor your functions to make them independent of members of the class - and thereby often creating a library of more general helper functions, which you can easly reuse elsewhere. I'd also mention that one of the common problems with many large projects is the bloating of classes - and I think preferring non-member, non-friend functions also helps here.