1

在文章非成员函数如何改进封装中,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 的作者,则该函数必须是 non-member,并且您必须这样称呼它:

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

Wombat w;    
nap(w);

你有它,你可怕的句法不一致。当你想喂你的袋熊时,你会调用成员函数,但是当你想让它们打盹时,你会调用非成员函数。

如果您稍微反思一下并对自己诚实,您就会承认您与您使用的所有重要类存在这种所谓的不一致,因为没有类具有每个客户所需的所有功能。每个客户都至少添加了一些自己的便利功能,这些功能始终是非成员的。C++ 程序员已经习惯了这一点,他们对此毫不在意。有些调用使用成员语法,有些调用使用非成员语法。人们只是查找适合他们想要调用的函数的语法,然后调用它们。生活仍在继续。它在标准 C++ 库的 STL 部分尤其如此,其中一些算法是成员函数(例如,大小),一些是非成员函数(例如,唯一),而一些是两者(例如,查找)。没有人眨眼。连你都没有。

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

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

4

3 回答 3

3

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

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

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

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

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

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

继续前面的例子,这意味着如果你想对一个对象进行 I/O,你不能支持该类型对象的复制构造或复制分配。

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

底线:我们几乎立刻就陷入了彻底无法管理的混乱局面,我们的继承将不再具有任何真正意义,编译器的类型检查将变得几乎完全无用。

于 2015-03-01T22:36:27.223 回答
1

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

我认为当迈耶斯第一次发表那篇文章时,我们大多数人都做了双重考虑,但现在人们普遍认为这是真的。在现代 C++ 的世界中,您的第一直觉应该是从类派生。派生是最后的手段,除非您要添加一个真正现有类的专业化的新类。

Java 中的情况有所不同。你在那里继承。你真的别无选择。

于 2015-03-01T21:29:19.187 回答
1

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

不过,有一些危险需要注意:

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

  • 基类指针- 如果Wombat是非多态的(即没有 v-table),这意味着你不能轻易地混合WombatmyWombat在一个容器中。删除指针不会正确删除myWombat品种。(但是您可以使用shared_ptrwhich 跟踪自定义删除器)。

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

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


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

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

你出现并myWombatWombat. 但是,这意味着它NorthernHairyNosedWombat是 的兄弟姐妹myWombat,而它是 的孩子Wombat

因此,您添加的任何漂亮的糖函数无论如何myWombat都无法使用NorthernHairyNosedWombat


总结:恕我直言,这些好处不值得它留下的烂摊子。

于 2015-03-01T23:38:34.840 回答