简体   繁体   中英

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".

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 (eg, size), some are non-member functions (eg, unique), and some are both (eg, 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?

I'm just starting out with C++, but that's how I would probably do it in Java. Is this not the way to go in 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).

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).

Second, it would render most inheritance meaningless. For an obvious example, many classes in C++ support I/O. As it stands now, the idiomatic way to do that is to overload operator<< and operator>> as free functions. 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. If we supported I/O via inheritance, it would also mean anything that can be read from/written to anything vaguely file-like.

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.

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.

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.

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).

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. Inheritance is not necessarily good; it is the second strongest relationship between any two entities in C++. Only friend declarations are stronger.

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. In the world of modern C++ your first instinct should not be to derive from a class. 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. 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.

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. This doesn't occur in Java in which all objects are passed by reference.

  • Base class pointer - If Wombat is non-polymorphic (ie no v-table), it means you cannot easily mix Wombat and myWombat in a container. Deleting a pointer will not properly delete myWombat varieties. (However you could use shared_ptr which tracks a custom deleter).

  • Type mismatch : If you write any functions that accept a myWombat then they cannot be called with a Wombat . On the other hand, if you write your function to accept a Wombat then you can't use the syntactic sugar of 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. This is more work in terms of design and maintenance of the myWombat class; 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 . However, this means that NorthernHairyNosedWombat is a sibling of myWombat , whereas it was a child of Wombat .

So any nice sugar functions you add to myWombat are not usable by NorthernHairyNosedWombat anyway.


Summary: IMHO the benefits are not worth the mess it leaves behind.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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