在文章How Non-Member Functions Improve Encapsulation中,Scott Meyers辩称,没有办法阻止非成员函数发生"。
语法问题
如果你和我讨论过这个问题的很多人一样,那你就是 可能对我的句法含义有所保留 建议非朋友非会员职能应该优先考虑 成员函数,即使你购买我关于封装的论点。对于 例如,假设一个类Wombat支持两者的功能 吃饭睡觉。进一步假设进食功能 必须实现为成员函数,但睡眠 功能可以作为成员或非朋友实现 非会员功能。如果你按照我上面的建议,你就会申报 这样的事情:
啊,这一切的均匀性!但这种统一性具有误导性, 因为世界上有比梦想更多的功能 你的哲学。class Wombat { public: void eat(double tonsToEat); void sleep(double hoursToSnooze); }; w.eat(.564); w.sleep(2.57);
说穿了,非会员功能发生了。让我们继续 袋熊的例子。假设你编写软件来模拟这些提取 生物,并想象你经常需要的东西之一 袋熊要做的就是睡半小时。显然,你可以 通过调用
w.sleep(.5)
来丢弃代码,但这会很多 .5s的类型,无论如何,如果该神奇的价值是什么 更改?有很多方法可以解决这个问题,但是 也许最简单的是定义一个封装的函数 你想做什么的细节。 假设您不是作者 Wombat,该功能必须是非会员 ,并且 你必须这样称呼它:void nap(Wombat& w) { w.sleep(.5); } Wombat w; nap(w);
你有它,你可怕的句法不一致。当你 想要喂你的袋熊,你做成员函数调用,但什么时候 你希望他们打盹,你打非会员电话。
如果你反思一点并对自己诚实,你就会承认这一点 你有所谓的与所有重要阶级的不一致 你使用,因为没有类具有每个客户所需的所有功能。 每个客户至少添加一些自己的便利功能, 并且这些功能始终是非成员。 C ++程序员习惯了 这个,他们什么都不想。有些调用使用成员语法,和 一些使用非成员语法。人们只是查找哪种语法 适合他们想要调用的函数,然后调用它们。 生活仍在继续。它继续特别是在标准的STL部分 C ++库,其中一些算法是成员函数(例如,大小), 一些是非成员函数(例如,唯一的),有些是(例如, 找)。没有人眨眼。甚至不是你。
我无法完全理解他在 粗体/斜体 句子中所说的内容。为什么必须以非会员身份实施?为什么不从Wombat类继承自己的MyWombat类,并使nap()
函数成为MyWombat的成员?
我刚刚开始使用C ++,但这就是我在Java中的表现。这不是C ++的方法吗?如果没有,为什么呢?
答案 0 :(得分:3)
理论上,你可以做到这一点,但你真的不想这样做。让我们考虑为什么你不想这样做(暂时,在原始上下文中 - C ++ 98/03,忽略C ++ 11及更新版本中的新增内容。)
首先,它意味着基本上所有类都必须被编写为基类 - 但对于某些类,这只是一个糟糕的想法,甚至可能直接违背基本意图(例如,某些东西)旨在实施Flyweight模式)。
其次,它会使大多数继承变得毫无意义。举一个明显的例子,C ++中的许多类都支持I / O.就目前而言,这样做的惯用方法是将operator<<
和operator>>
重载为自由函数。现在,iostream的意图是表示至少模糊文件的东西 - 我们可以写入数据的东西,和/或我们可以从中读取数据的东西。如果我们通过继承支持I / O,那么它也意味着可以从任何模糊文件中读取/写入任何内容。
这完全没有任何意义。 iostream代表至少模糊文件的东西,而不是你可能想要读取或写入文件的所有类型的对象。
更糟糕的是,它会使几乎所有编译器的类型检查几乎毫无意义。例如,将distance
对象写入person
对象是没有意义的 - 但如果它们都是通过从iostream派生来支持I / O,那么编译器就没有办法排序那是一个真正有意义的。
不幸的是,这只是冰山一角。从基类继承时,将继承该基类的 limits 。例如,如果您使用的是不支持复制赋值或复制构造的基类,则派生类的对象也不会/不能。
继续上一个示例,这意味着如果要对对象执行I / O,则不能支持该类型对象的复制构造或复制分配。
反过来,这意味着支持I / O的对象将与支持放入集合的对象(即集合需要 iostream禁止的功能)脱节。
结论:我们几乎立刻就会陷入彻底难以控制的混乱状态,在这种情况下,我们的继承都不再具有任何真正的意义,编译器的类型检查几乎完全没用。
答案 1 :(得分:1)
因为您在新类和原始Wombat之间创建了非常强大的依赖关系。继承不一定好;它是C ++中任意两个实体之间的第二强关系。只有friend
声明更强。
我认为,当迈耶斯首次发表这篇文章时,我们大多数人都做了双重考虑,但现在人们普遍认为这是真的。在现代C ++世界中,你的第一直觉应该不来自一个类。派生是最后的选择,除非你要添加一个真正是现有类专业化的新类。
Java中的问题不同。你继承了。你别无选择。
答案 2 :(得分:1)
你的想法并不像Jerry Coffin描述的那样全面运作,但是对于不属于层次结构的简单类来说它是可行的,例如Wombat
。
但有几点需要注意:
切片 - 如果有一个按值接受Wombat
的功能,那么您必须切断myWombat
的额外附属物,他们不会长大。这并不是在Java中发生的,其中所有对象都通过引用传递。
基类指针 - 如果Wombat
是非多态的(即没有v-table),则意味着您无法轻松混合Wombat
和{{1在一个容器中。删除指针将无法正确删除myWombat
种类。 (但是,您可以使用跟踪自定义删除工具的myWombat
。)
类型不匹配:如果您编写任何接受shared_ptr
的函数,则无法使用myWombat
调用它们。另一方面,如果你写函数接受Wombat
,那么你就不能使用Wombat
的句法糖。施法并没有解决这个问题;您的代码无法与界面的其他部分正常交互。
避免所有这些危险的方法是使用包含而不是继承:myWombat
将拥有myWombat
私有成员,并且您为要公开的任何Wombat属性编写转发函数。在Wombat
类的设计和维护方面,这是更多的工作;但是它消除了任何人错误地使用你的类的可能性,并且它使你能够解决诸如所包含的类是不可复制的问题。
对于层次结构中的多态对象,虽然类型不匹配问题仍然存在,但您不会遇到切片和基类指针问题。事实上情况更糟。假设层次结构是:
myWombat
您来自Animal <-- Marsupial <-- Wombat <-- NorthernHairyNosedWombat
并从myWombat
派生Wombat
。但是,这意味着NorthernHairyNosedWombat
是myWombat
的兄弟,而它是Wombat
的孩子。
因此,myWombat
无法使用添加到NorthernHairyNosedWombat
的任何优质糖功能。
摘要:恕我直言,这些好处不值得它留下的烂摊子。