非友元、非成员函数增加封装?

Non-friend, non-member functions increase encapsulation?

在文章 How Non-Member Functions Improve Encapsulation 中,Scott Meyers 认为没有办法阻止非成员函数 "happening"。

Syntax Issues

If you're like many people with whom I've discussed this issue, you're likely to have reservations about the syntactic implications of my advice that non-friend non-member functions should be preferred to member functions, even if you buy my argument about encapsulation. For example, suppose a class Wombat supports the functionality of both eating and sleeping. Further suppose that the eating functionality must be implemented as a member function, but the sleeping functionality could be implemented as a member or as a non-friend non-member function. If you follow my advice from above, you'd declare things like this:

class Wombat {
public:
   void eat(double tonsToEat);
   void sleep(double hoursToSnooze);
};

w.eat(.564);
w.sleep(2.57);

Ah, the uniformity of it all! But this uniformity is misleading, because there are more functions in the world than are dreamt of by your philosophy.

To put it bluntly, non-member functions happen. Let us continue with the Wombat example. Suppose you write software to model these fetching creatures, and imagine that one of the things you frequently need your Wombats to do is sleep for precisely half an hour. Clearly, you could litter your code with calls to w.sleep(.5), but that would be a lot of .5s to type, and at any rate, what if that magic value were to change? There are a number of ways to deal with this issue, but perhaps the simplest is to define a function that encapsulates the details of what you want to do. Assuming you're not the author of Wombat, the function will necessarily have to be a non-member, and you'll have to call it as such:

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

Wombat w;    
nap(w);

And there you have it, your dreaded syntactic inconsistency. When you want to feed your wombats, you make member function calls, but when you want them to nap, you make non-member calls.

If you reflect a bit and are honest with yourself, you'll admit that you have this alleged inconsistency with all the nontrivial classes you use, because no class has every function desired by every client. Every client adds at least a few convenience functions of their own, and these functions are always non-members. C++ programers are used to this, and they think nothing of it. Some calls use member syntax, and some use non-member syntax. People just look up which syntax is appropriate for the functions they want to call, then they call them. Life goes on. It goes on especially in the STL portion of the Standard C++ library, where some algorithms are member functions (e.g., size), some are non-member functions (e.g., unique), and some are both (e.g., find). Nobody blinks. Not even you.

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

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

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

我想当迈耶斯第一次发表那篇文章时,我们大多数人都怀疑过,但现在人们普遍认为这是真的。在现代 C++ 的世界中,您的第一直觉应该 而不是 派生自 class。派生是最后的手段,除非您要添加一个新的class,它实际上是现有class的特化.

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

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

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

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

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

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

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

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

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

底线:我们几乎立即陷入完全无法管理的混乱局面,其中 none 我们的继承将不再有任何实际意义,编译器的类型检查将变得几乎完全无用。

正如 Jerry Coffin 所描述的那样,您的想法并不适用于所有领域,但是它对于不属于层次结构的简单 class 是可行的,例如此处的 Wombat

虽然有一些危险需要注意:

  • Slicing - 如果有函数按值接受Wombat,那么你必须切断myWombat'额外的附属物,它们不会再长出来。这不会发生在 Java 中,其中所有 objects 都是通过引用传递的。

  • Base class指针 - 如果Wombat是non-polymorphic(即没有v-table),这意味着您不能轻易地将 WombatmyWombat 混合在一个容器中。删除指针不会正确删除 myWombat 品种。 (但是您可以使用 shared_ptr 跟踪自定义删除器)。

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

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


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

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

你来了,从 Wombat 推导出 myWombat。但是,这意味着 NorthernHairyNosedWombatmyWombat 的兄弟姐妹,而它是 Wombat 的 child。

因此,您添加到 myWombat 的任何漂亮的糖功能无论如何都无法被 NorthernHairyNosedWombat 使用。


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