非朋友,非会员功能增加封装?

时间:2015-03-01 21:18:44

标签: c++ encapsulation member non-member-functions

在文章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 ++的方法吗?如果没有,为什么呢?

3 个答案:

答案 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。但是,这意味着NorthernHairyNosedWombatmyWombat的兄弟,而它是Wombat的孩子。

因此,myWombat无法使用添加到NorthernHairyNosedWombat的任何优质糖功能。


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