42

在对类设计的一些事实感到困惑时,特别是函数是否应该是成员,我查看了 Effective c++ 并找到了第 23 条,即 Prefer non-member non-friend functions to member functions。使用 Web 浏览器示例第一手阅读该内容是有一定道理的,但是该示例中的便利函数(在书中像这样命名非成员函数)会改变类的状态,不是吗?

  • 那么,第一个问题,他们不应该成为会员吗?

  • 进一步阅读,他考虑了 STL 函数,实际上某些类未实现的一些函数是在 stl 中实现的。按照本书的想法,它们演变成一些方便的函数,这些函数被打包到一些合理的命名空间中std::sort,例如std::copyfrom algorithm。例如vector,类没有sort函数,并且使用 stlsort函数,因此它不是向量类的成员。但是也可以将相同的推理扩展到向量类中的其他一些函数,例如,assign它也不能作为成员实现,而是作为便利函数实现。然而,这也会改变对象的内部状态,比如它所操作的排序。那么这个微妙但重要(我猜)的问题背后的基本原理是什么。

如果您可以访问这本书,您能否为我进一步澄清这些观点?

4

7 回答 7

43

访问这本书绝不是必要的。

我们在这里处理的问题是依赖重用

在设计良好的软件中,您尝试将项目彼此隔离以减少依赖关系,因为当需要更改时,依赖关系是一个需要克服的障碍。

在设计良好的软件中,您应用DRY原则(不要重复自己),因为当需要进行更改时,必须在十几个不同的地方重复它是痛苦且容易出错的。

“经典” OO 思维方式在处理依赖关系方面越来越糟糕。通过直接依赖于类内部的大量方法,最轻微的变化就意味着整个重写。不必如此。

在 C++ 中,STL(不是整个标准库)的设计明确目标是:

  • 削减依赖关系
  • 允许重用

因此,容器公开了定义良好的接口,这些接口隐藏了它们的内部表示,但仍然提供对它们封装的信息的充分访问,以便可以在它们上执行算法。所有修改都是通过容器接口进行的,以保证不变量。

例如,如果您考虑sort算法的要求。对于 STL(通常)使用的实现,它需要(来自容器):

  • 有效访问给定索引处的项目:随机访问
  • 交换两个项目的能力:不是关联的

因此,任何提供随机访问且非关联的容器(理论上)都适合通过(例如)快速排序算法进行有效排序。

满足这一点的 C++ 容器有哪些?

  • 基本的 C 数组
  • deque
  • vector

如果您注意这些细节,可能会编写任何容器。

sort为每一个重写(复制/粘贴/调整)会很浪费,不是吗?

请注意,例如,有一种std::list::sort方法。为什么 ?因为std::list不提供随机访问(非正式地myList[4]不起作用),因此sortfrom 算法不适合。

于 2011-05-13T11:25:44.180 回答
20

我使用的标准是,如果一个函数可以通过成为一个成员函数来更有效地实现,那么它应该是一个成员函数。::std::sort不符合该定义。事实上,在外部和内部实施它没有任何效率差异。

通过将某些东西实现为成员(或朋友)函数来极大地提高效率意味着它极大地受益于了解类的内部状态。

接口设计艺术的一部分是找到最小的成员函数集的艺术,以便您可能想要对对象执行的所有操作都可以根据它们合理有效地实现。而且这个集合不应该支持不应该在类上执行的操作。所以你不能仅仅实现一堆 getter 和 setter 函数就称它为好。

于 2011-05-13T09:36:01.220 回答
11

我认为这条规则的原因是,通过使用成员函数,您可能会意外地过度依赖类的内部结构。更改类的状态不是问题。真正的问题是,如果您修改类中的一些私有属性,您需要更改的代码量。保持类的接口(公共方法)尽可能小,既可以减少您在这种情况下需要做的工作量,也可以减少对私有数据做一些奇怪的事情的风险,从而使您的实例处于不一致的状态.

AtoMerZ 也是对的,非成员非好友函数也可以模板化并重用于其他类型。

顺便说一句,你应该购买一本 Effective C++,这是一本很棒的书,但不要试图总是遵守本书的每一项内容。面向对象设计既有好的实践(来自书籍等)和经验(我认为它也在某处用有效的 C++ 编写)。

于 2011-05-13T09:26:01.617 回答
6

各种想法:

  • 当非成员通过类的公共 API 工作时,这很好,因为它减少了以下代码量:
    • 需要仔细监控以确保类不变量,
    • 如果重新设计对象的实现,则需要更改。
  • 当这还不够好时,非会员仍然可以成为friend.
  • 编写非成员函数通常不太方便,因为成员并不隐含在范围内,但是如果您考虑程序演化:
    • 一旦存在非成员函数并且意识到相同的功能对其他类型有用,通常很容易将函数转换为模板并使其不仅适用于两种类型,而且适用于任意未来类型。换句话说,非成员模板允许比运行时多态性/虚拟分派更灵活的算法重用:模板允许称为鸭子类型的东西。
    • 具有有用成员函数的现有类型鼓励剪切和粘贴到想要类似行为的其他类型,因为转换函数以供重用的大多数方法都要求每个隐式成员访问都成为对特定对象的显式访问,对于程序员来说,这将是一个更乏味的 30+ 秒....
  • 成员函数允许使用object.function(x, y, z)符号,恕我直言,它非常方便、富有表现力和直观。它们还可以更好地与许多 IDE 中的发现/完成功能配合使用。
  • 成员函数和非成员函数的分离有助于传达类的基本性质、不变量和基本操作,并在逻辑上对附加功能和可能的临时“便利”功能进行分组。考虑一下 Tony Hoare 的智慧:

    “构建软件设计有两种方法:一种方法是让它变得如此简单以至于没有明显的缺陷,另一种方法是让它变得如此复杂以至于没有明显的缺陷。第一种方法要困难得多。”

    • 在这里,非会员的使用不一定要困难得多,但您确实必须更多地考虑您如何访问会员数据和私有/受保护的方法,以及为什么,以及哪些操作是基本的。这样的灵魂搜索也会改进成员函数的设计,只是更容易偷懒:-/。
  • 随着非成员功能的复杂性扩展或增加额外的依赖项,这些功能可以移动到单独的头文件和实现文件,甚至是库中,因此核心功能的用户只需“付费”使用他们想要的部分。

(Omnifarious 的答案是必读的,如果您不熟悉,请阅读三次。)

于 2011-05-26T08:59:24.603 回答
4

动机很简单:保持一致的语法。随着类的演进或使用,会出现各种非成员便利功能;例如,您不想修改类接口以添加类似toUpper字符串类的内容。(当然,在 的情况下 std::string,你不能。)斯科特担心的是,当这种情况发生时,你最终会得到不一致的语法:

s.insert( "abc" );
toUpper( s );

通过仅使用自由函数,根据需要将它们声明为友元,所有函数都具有相同的语法。另一种方法是在每次添加便利函数时修改类定义。

我并不完全相信。如果一个类设计得很好,它就有一个基本功能,用户很清楚哪些功能是该基本功能的一部分,哪些是附加的便利功能(如果存在的话)。在全球范围内,字符串是一种特殊情况,因为它被设计用于解决许多不同的问题。我无法想象许多班级都是这种情况。

于 2011-05-13T09:42:33.057 回答
4

那么,第一个问题,他们不应该是会员比吗?

不,这不符合。在惯用的 C++ 类设计中(至少在Effective C++中使用的惯用语中),非成员非友元函数扩展了类接口。它们可以被认为是该类的公共 API 的一部分,尽管它们不需要也没有对该类的私有访问权。如果根据 OOP 的某些定义,这种设计“不是 OOP”,那么,好的,惯用的 C++ 不是该定义的 OOP。

将相同的推理扩展到向量类中的一些其他函数

没错,标准容器的一些成员函数可能是自由函数。例如vector::push_back是根据 定义的insert,当然可以在没有对类的私有访问的情况下实现。但是,在这种情况下,它push_back是一个抽象概念的一部分BackInsertionSequence,即 vector 实现的。这样的通用概念跨越了特定类的设计,因此如果您正在设计或实现自己的通用概念,可能会影响您放置函数的位置。

当然,标准的某些部分可以说应该是不同的,例如std::string has way too many member functions。但是已经完成了,这些类是在人们真正适应我们现在可能称之为现代 C++ 风格的之前设计的。无论哪种方式,该课程都有效,因此您只能从担心差异中获得很多实际好处。

于 2011-05-13T10:31:06.673 回答
1

我认为 sort 不是作为成员函数实现的,因为它被广泛使用,而不仅仅是向量。如果他们把它作为一个成员函数,他们每次都必须为每个使用它的容器重新实现它。所以我认为这是为了更容易实施。

于 2011-05-13T09:21:18.127 回答