非好友、非成员函数增加了封装

Non-friend, non-member functions increase encapsulation?

本文关键字:封装 增加 成员 好友 函数      更新时间:2023-10-16

在《非成员函数如何改进封装》一文中,Scott Meyers认为没有办法阻止非成员函数"发生"。

语法问题

如果你和我讨论过这个问题的许多人一样可能对我的建议非好友非会员功能应优先成员函数,即使你相信我关于封装的论点。对于例如,假设一个类Wombat支持吃饭和睡觉。进一步假设进食功能必须作为成员函数来实现,但是睡眠功能可以作为成员或非朋友来实现非成员函数。如果你听从我上面的建议,你会声明像这样的东西:

class Wombat {
public:
   void eat(double tonsToEat);
   void sleep(double hoursToSnooze);
};
w.eat(.564);
w.sleep(2.57);

啊,这一切的统一!但这种一致性具有误导性,因为世界上有比做梦都想不到的更多的功能你的哲学。

坦率地说,非成员职能是发生的。让我们继续Wombat的例子。假设您编写软件来模拟这些抓取想象一下,你经常需要的东西之一袋熊要做的就是睡半个小时。很明显,你可以代码中充斥着对w.sleep(.5)的调用,但这将是一个很大的问题0.5秒的时间来打字,无论如何,如果这个神奇的值是改变有很多方法可以解决这个问题,但是也许最简单的方法是定义一个函数来封装你想做什么的详细信息。假设你不是Wombat,函数必须是非成员,并且你必须这样称呼它:

void nap(Wombat& w) { w.sleep(.5); }
Wombat w;    
nap(w);

你就知道了,你可怕的句法不一致。当你想要养活你的袋熊,你可以调用成员函数,但当你想让他们打个盹,你就给非会员打电话。

如果你反思一下,对自己诚实一点,你就会承认你有这种所谓的与所有非平凡类的不一致使用,因为没有一个类具有每个客户端所需的所有函数。每个客户端都添加了至少一些自己的便利功能,并且这些功能总是非成员的。C++程序用于有些调用使用成员语法,并且有些使用非成员语法。人们只是查一下哪种语法是适用于他们想要调用的函数,然后他们调用它们。生活还在继续,尤其是在标准的STL部分C++库,其中一些算法是成员函数(例如大小),一些是非成员函数(例如唯一)。,查找)。没有人眨眼。甚至连你都没有。

我真的无法理解他在粗体/斜体句子中所说的话。为什么它必须作为非成员国实施?为什么不从Wombat类继承您自己的MyWombat,并使nap()函数成为MyWombat的成员呢?

我刚开始使用C++,但在Java中可能就是这样做的。这不是C++的发展方向吗?如果没有,为什么?

理论上,你可以这样做,但你真的不想这样做。让我们考虑一下你为什么不想这么做(目前,在原始上下文中——C++98/03,忽略C++11和更新版本中的添加)。

首先,这意味着基本上所有的类都必须被编写为基类——但对于一些类来说,这只是一个糟糕的想法,甚至可能直接违背基本意图运行(例如,旨在实现Flyweight模式的东西)。

其次,这将使大多数继承变得毫无意义。举个明显的例子,C++中的许多类都支持I/O。按照目前的情况,实现这一点的惯用方法是将operator<<operator>>作为自由函数重载。现在,iostream的目的是表示一些至少模糊地类似于文件的东西——我们可以向其中写入数据,和/或从中读取数据。如果我们通过继承支持I/O,那么这也意味着可以从任何模糊的文件中读取/写入任何模糊的东西。

这根本没有任何意义。iostream表示的至少是模糊的类似文件的东西,而不是你可能想从文件中读取或写入的所有类型的对象。

更糟糕的是,它会使几乎所有编译器的类型检查几乎毫无意义。例如,将distance对象写入person对象是没有意义的——但如果它们都通过从iostream派生来支持I/O,那么编译器将无法从真正有意义的对象中进行排序。

不幸的是,这只是冰山一角。从基类继承时,将继承该基类的限制。例如,如果您使用的基类不支持复制赋值或复制构造,则派生类的对象也不会/不能。

继续前面的示例,这意味着如果要对对象执行I/O操作,则不能支持该类型对象的复制构造或复制分配。

这反过来意味着,支持I/O的对象将与支持放入集合的对象分离(即,集合需要iostream禁止的功能)。

一句话:我们几乎立即陷入了一个完全无法管理的混乱,我们的继承将不再有任何真正的意义,编译器的类型检查将变得几乎完全无用。

因为您将在新类和原始Wombat之间创建一个非常强的依赖关系。继承不一定是好的;它是C++中任何两个实体之间第二强的关系。只有friend声明更强。

我想,当迈耶斯第一次发表这篇文章时,我们大多数人都有双重看法,但现在人们普遍认为这是真的。在现代C++的世界里,你的第一直觉应该是从类派生。派生是最后的手段,除非您添加的新类实际上是对现有类的专门化。

Java中的情况不同。你继承了。你真的别无选择。

正如Jerry Coffin所描述的,您的想法并不能全面发挥作用,但它对于不属于层次结构的简单类是可行的,例如这里的Wombat

不过,有几个危险需要注意:

  • 切片-如果有一个函数按值接受Wombat,那么你必须切断myWombat的额外附属物,它们就不会长回来。在Java中不会发生这种情况,在Java中,所有对象都是通过引用传递的。

  • 基类指针-如果Wombat是非多态的(即没有v-table),则意味着您无法在容器中轻松混合WombatmyWombat。删除指针不会正确删除myWombat品种。(但是,您可以使用shared_ptr来跟踪自定义删除程序)。

  • 类型不匹配:如果编写任何接受myWombat的函数,则不能用Wombat调用它们。另一方面,如果您编写函数来接受Wombat,那么就不能使用myWombat的语法糖。铸造并不能解决这个问题;您的代码将无法与接口的其他部分正确交互。

避免所有这些危险的一种方法是使用包含而不是继承myWombat将有一个Wombat私有成员,您可以为要公开的任何Wombat属性编写转发函数。这是在myWombat级的设计和维护方面的更多工作;但它消除了任何人错误使用类的可能性,并使您能够解决诸如包含的类不可复制之类的问题。


对于层次结构中的多态对象,虽然类型不匹配的问题仍然存在,但不存在切片和基类指针问题。事实上情况更糟。假设层次结构为:

Animal <-- Marsupial <-- Wombat <-- NorthernHairyNosedWombat

您将从Wombat派生出myWombat。然而,这意味着NorthernHairyNosedWombatmyWombat的兄弟姐妹,而它是Wombat的孩子。

因此,添加到myWombat中的任何漂亮的糖函数无论如何都不能被NorthernHairyNosedWombat使用。


总结:IMHO的好处不值得它留下的烂摊子。