简体   繁体   English

非朋友,非成员函数会增加封装?

[英]Non-friend, non-member functions increase encapsulation?

In the article How Non-Member Functions Improve Encapsulation , Scott Meyers argues that there is no way to prevent non-member functions from "happening". 非成员函数如何改善封装的文章中,Scott Meyers提出无法阻止非成员函数“发生”。

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. 例如,假设Wombat类支持进食和睡眠功能。 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? 显然,您可以使用对w.sleep(.5)调用来w.sleep(.5)代码,但这要输入大量的.5,无论如何,如果魔术值发生了变化该怎么办? 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: 假设您不是Wombat的作者,则该函数必须必须是non-member ,并且您必须这样调用它:

 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. C ++程序员习惯了这一点,他们对此一无所知。 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 (eg, size), some are non-member functions (eg, unique), and some are both (eg, find). 它在标准C ++库的STL部分中尤其如此,其中一些算法是成员函数(例如,大小),一些算法是非成员函数(例如,唯一),而某些都是(例如,find)。 Nobody blinks. 没有人眨眼。 Not even you. 甚至没有你

I can't really wrap my head around what he says in the bold/italic sentence. 我真的不能把他的粗体/斜体字说的话全神贯注 Why will it necessarily have to be implemented as a non-member? 为什么必须将其作为非成员实施? Why not just inherit your own MyWombat class from the Wombat class, and make the nap() function a member of MyWombat? 为什么不只从Wombat类继承您自己的MyWombat类,并使nap()函数成为MyWombat的成员呢?

I'm just starting out with C++, but that's how I would probably do it in Java. 我刚开始使用C ++,但这就是我可能会用Java做到的方式。 Is this not the way to go in C++? 这不是使用C ++的方法吗? If not, why so? 如果没有,为什么呢?

In theory, you sort of could do this, but you really don't want to. 从理论上讲,您可以做到这一点,但您确实不想这么做。 Let's consider why you don't want to do this (for the moment, in the original context--C++98/03, and ignoring the additions in C++11 and newer). 让我们考虑一下为什么您不想这样做(目前,在原始上下文中-C ++ 98/03,而忽略了C ++ 11和更高版本中的新增功能)。

First of all, it would mean that essentially all classes have to be written to act as base classes--but for some classes, that's just a lousy idea, and may even run directly contrary to the basic intent (eg, something intended to implement the Flyweight pattern). 首先,这意味着基本上所有类都必须编写为充当基类,但是对于某些类来说,这只是一个糟糕的想法,甚至可能与基本意图背道而驰(例如,旨在实现的某些东西) Flyweight模式)。

Second, it would render most inheritance meaningless. 其次,它将使大多数继承毫无意义。 For an obvious example, many classes in C++ support I/O. 举一个明显的例子,C ++中的许多类都支持I / O。 As it stands now, the idiomatic way to do that is to overload operator<< and operator>> as free functions. 就目前而言,惯用的方法是将operator<<operator>>重载为自由函数。 Right now, the intent of an iostream is to represent something that's at least vaguely file-like--something into which we can write data, and/or out of which we can read data. 目前,iostream的目的是代表至少某种类似于文件的东西-我们可以在其中写入数据,和/或我们可以从其中读取数据的东西。 If we supported I/O via inheritance, it would also mean anything that can be read from/written to anything vaguely file-like. 如果我们通过继承支持I / O,那也意味着可以从任何模糊的文件中读取/写入的内容。

This simply makes no sense at all. 这根本没有任何意义。 An iostream represents something at least vaguely file-like, not all the kinds of objects you might want to read from or write to a file. iostream至少代表某种类似于文件的东西,而不是您可能想要从文件读取或写入文件的所有对象。

Worse, it would render nearly all the compiler's type checking nearly meaningless. 更糟糕的是,这将使几乎所有编译器的类型检查几乎毫无意义。 Just for example, writing a distance object into a person object makes no sense--but if they both support I/O by being derived from iostream, then the compiler wouldn't have a way to sort that out from one that really did make sense. 仅举例来说,将distance对象写入person对象没有任何意义-但是,如果它们都通过从iostream派生来支持I / O,则编译器将无法从真正使感。

Unfortunately, that's just the tip of the iceberg. 不幸的是,那只是冰山一角。 When you inherit from a base class, you inherit the limitations of that base class. 从基类继承时,您将继承该基类的限制 For example, if you're using a base class that doesn't support copy assignment or copy construction, objects of the derived class won't/can't either. 例如,如果您使用的基类不支持副本分配或副本构造,则派生类的对象也不会/也不能够。

Continuing the previous example, that would mean if you want to do I/O on an object, you can't support copy construction or copy assignment for that type of object. 继续前面的示例,这意味着如果要对对象执行I / O,则不能支持该类型对象的副本构造或副本分配。

That, in turn, means that objects that support I/O would be disjoint from objects that support being put in collections (ie, collections require capabilities that are prohibited by iostreams). 反过来,这意味着支持I / O的对象将与支持放入集合中的对象脱节(即,集合需要 iostream禁止的功能)。

Bottom line: we almost immediately end up with a thoroughly unmanageable mess, where none of our inheritance would any longer make any real sense at all and the compiler's type checking would be rendered almost completely useless. 底线:我们几乎立即以彻底难以处理的混乱结束,在此混乱中,我们的继承都不再具有任何实际意义,并且编译器的类型检查将几乎完全变得无用。

Because you are then creating a very strong dependency between your new class and the original Wombat. 因为这样您就在新类和原始Wombat之间建立了非常强烈的依赖关系。 Inheritance is not necessarily good; 继承不一定是好的。 it is the second strongest relationship between any two entities in C++. 它是C ++中任何两个实体之间第二强的关系。 Only friend declarations are stronger. 只有friend声明更强。

I think most of us did a double-take when Meyers first published that article, but it is generally acknowledged to be true by now. 我认为当Meyers首次发表该文章时,我们大多数人都采取了双重行动,但目前人们普遍认为它是真的。 In the world of modern C++ your first instinct should not be to derive from a class. 在现代C的世界++你的第一反应应该是从类派生。 Deriving is the last resort, unless you are adding a new class that really is a specialization of an existing class. 除非您要添加确实现有类专业化的新类, 否则派生是万不得已的方法。

Matters are different in Java. Java中的事项有所不同。 There you inherit. 您在那里继承。 You really have no other choice. 您真的别无选择。

Your idea doesn't work across the board, as Jerry Coffin describes, however it is viable for simple classes that are not part of a hierarchy, such as Wombat here. 正如杰里·科芬(Jerry Coffin)所描述的那样,您的想法并不全面,但是对于不属于层次结构的简单类(例如此处的Wombat ,它是可行的。

There are some couple of dangers to watch out for though: 但是,有一些危险需要提防:

  • Slicing - if there is a function that accepts a Wombat by value, then you have to cut off myWombat 's extra appendages and they don't grow back. 切片 -如果存在一个按值接受Wombat的函数,则您必须切断myWombat的额外附件,并且它们不会重新增长。 This doesn't occur in Java in which all objects are passed by reference. 在Java中通过引用传递所有对象的情况下不会发生这种情况。

  • Base class pointer - If Wombat is non-polymorphic (ie no v-table), it means you cannot easily mix Wombat and myWombat in a container. 基类指针 -如果Wombat是非多态的(即没有v-table),则意味着您不能轻松地在容器中混合WombatmyWombat Deleting a pointer will not properly delete myWombat varieties. 删除指针不会正确删除myWombat品种。 (However you could use shared_ptr which tracks a custom deleter). (但是,您可以使用shared_ptr来跟踪自定义删除器)。

  • Type mismatch : If you write any functions that accept a myWombat then they cannot be called with a Wombat . 类型不匹配 :如果编写任何接受myWombat函数,则无法使用Wombat调用它们。 On the other hand, if you write your function to accept a Wombat then you can't use the syntactic sugar of myWombat . 另一方面,如果编写函数以接受Wombat则不能使用myWombat的语法糖。 Casting doesn't fix this; 投射无法解决此问题; your code won't interact properly with other parts of the interface. 您的代码将无法与界面的其他部分正确交互。

A way of avoiding all these dangers would be to use containment instead of inheritance : myWombat will have a Wombat private member, and you write forwarding functions for any Wombat properties you want to expose. 避免所有这些危险的一种方法是使用包容而不是继承myWombat将具有Wombat私有成员,并且为要公开的任何Wombat属性编写转发功能。 This is more work in terms of design and maintenance of the myWombat class; myWombat类的设计和维护方面,这是更多工作。 but it eliminates the possibility for anyone to use your class erroneously, and it enables you to work around problems such as the contained class being non-copyable. 但是它消除了任何人错误使用您的类的可能性,并且使您能够解决诸如所包含的类不可复制之类的问题。


For polymorphic objects in a hierarchy, you don't have the slicing and base-class-pointer problems, although the type mismatch problem is still there. 对于层次结构中的多态对象,尽管类型不匹配问题仍然存在,但您没有切片和基类指针问题。 In fact it's worse. 实际上,情况更糟。 Suppose the hierarchy is: 假设层次结构为:

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

You come along and derive myWombat from Wombat . 您可以从Wombat派生myWombat However, this means that NorthernHairyNosedWombat is a sibling of myWombat , whereas it was a child of Wombat . 但是,这意味着NorthernHairyNosedWombatmyWombat的同胞,而它是Wombat的子代。

So any nice sugar functions you add to myWombat are not usable by NorthernHairyNosedWombat anyway. 因此,无论如何, NorthernHairyNosedWombat都无法使用添加到myWombat中的任何漂亮的myWombat函数。


Summary: IMHO the benefits are not worth the mess it leaves behind. 简介:恕我直言,这些好处不值得它留下的烂摊子。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM