Effective C++ Item 23 Prefer non-member non-friend functions to member functions
Asked Answered
T

7

46

While puzzling with some facts on class design, specifically whether the functions should be members or not, I looked into Effective c++ and found Item 23, namely, Prefer non-member non-friend functions to member functions. Reading that at first hand with the web browser example made some sense, however convenience functions( named the nonmember functions like this in the book) in that example change the state of the class, don't they?

  • So, first question, should not they be members then?

  • Reading a bit further, he considers the STL functions and indeed some functions which are not implemented by some classes are implemented in stl. Following the ideas of the book they evolve into some convenience functions that are packed into some reasonable namespaces such as std::sort, std::copy from algorithm. For instance vector class does not have a sort function and one uses the stl sort function so that is not a member of the vector class. But one could also stretch the same reasoning to some other functions in vector class such as assign so that could also not be implemented as a member but as a convenience function. However that also changes the internal state of the object like sort on which it operated. So what is the rationale behind this subtle but important (I guess) issue.

If you have access to the book can you clarify these points a bit more for me?

Tymothy answered 13/5, 2011 at 9:14 Comment(1)
I'm amazed that nobody posted the link to the very relevent Dr Dobbs Article by Scott Meyer yet!Thoron
B
47

Access to the book is by no mean necessary.

The issues we are dealing here are Dependency and Reuse.

In a well-designed software, you try to isolate items from one another so as to reduce Dependencies, because Dependencies are a hurdle to overcome when change is necessary.

In a well-designed software, you apply the DRY principle (Don't Repeat Yourself) because when a change is necessary, it's painful and error-prone to have to repeat it in a dozen different places.

The "classic" OO mindset is increasingly bad at handling dependencies. By having lots and lots of methods depending directly on the internals of the class, the slightest change implies a whole rewrite. It need not be so.

In C++, the STL (not the whole standard library), has been designed with the explicit goals of:

  • cutting dependencies
  • allowing reuse

Therefore, the Containers expose well-defined interfaces that hide their internal representations but still offer sufficient access to the information they encapsulate so that Algorithms may be executed on them. All modifications are made through the container interface so that the invariants are guaranteed.

For example, if you think about the requirements of the sort algorithm. For the implementation used (in general) by the STL, it requires (from the container):

  • efficient access to an item at a given index: Random Access
  • the ability to swap two items: not Associative

Thus, any container that provides Random Access and is not Associative is (in theory) suitable to be sorted efficiently by (say) a Quick Sort algorithm.

What are the Containers in C++ that satisfy this ?

  • the basic C-array
  • deque
  • vector

And any container that you may write if you pay attention to these details.

It would be wasteful, wouldn't it, to rewrite (copy/paste/tweak) sort for each of those ?

Note, for example, that there is a std::list::sort method. Why ? Because std::list does not offer random access (informally myList[4] does not work), thus the sort from algorithm is not suitable.

Berserk answered 13/5, 2011 at 11:25 Comment(6)
There is no particular reason why having lots of member functions necessarily implies lots of accesses to private members of a class. These are two orthogonal issues. Member functions can call other member functions in exactly the same cases as external nonmember functions do: for instance, a small set of "core" member functions can directly access private data, and other member functions can indirectly access data through them. This problem is neither made better nor worse by making the functions nonmember functions. Whoever is maintaining the class needs to enforce the invariants regardless.Resuscitator
@SomeGuy: You are technically correct, but also missing the point. Member functions may access internals, whereas non-member non-friends cannot. Even if they don't now, they may in the future. The advice is therefore pushing for higher encapsulation by design -- promoting a minimal interface having to maintain invariants.Berserk
I'm not missing the point. I disagree with it. I think that the problems created by this approach are worse than the problems it is intended to solve. People who have the ability to modify part of a class usually can modify all of it as well as nonmember functions in the same file, so these changes aren't really preventing these kinds of code changes any more than a code comment would. However, this division is VISIBLE TO THE CLIENTS OF THE CLASS, which violates encapsulation by making changes that could otherwise be "internal only" instead affect the class's external interface.Resuscitator
If the goal is to have a set of core functions that do the "real work" and have access to private data members, and also have a bunch of public functions that use those, then a better approach is to have an inner private subobject with core functions and data, and a public outer object. That way, which functions are "core" and which are "non-core" can change over time without affecting the outer object's external interface which clients depend on: the outer object's guts and inner object's def can be changed over time without affecting clients: true separation of interface and implementation.Resuscitator
@SomeGuy: You are correct that using an inner class for the state gives the same encapsulation benefits; it's actually something I recommend when dealing with "mutex" protected state -- by having the public method lock the mutex then call a method on the inner class, the bulk of potential re-entrancy issues is immediately avoided. You are also correct that the user can tell the difference between member and non-member, and that any change is visible, and if such changes are unwanted indeed your design is superior (though... YAGNI?) ...Berserk
@SomeGuy: On the other hand, there is a definite advantage to non-member functions => template non-member functions are reusable. The STL algorithms mentioned in the OP are a prime example, nobody wants to rewrite sort for every container if they can avoid it. Going further, ADL makes it possible to seamlessly call either a generic template function or a specialized function in a template function -- something which doesn't work as painlessly with member functions -- and a primary example is the use std::swap; swap(x, y);. The guideline has the advantage of simplicity and composability.Berserk
O
22

The criteria I use is if a function could be implemented significantly more efficiently by being a member function, then it should be a member function. ::std::sort does not meet that definition. In fact, there is no efficiency difference whatsoever in implementing it externally vs. internally.

A vast efficiency improvement by implementing something as a member (or friend) function means that it greatly benefits from knowing the internal state of the class.

Part of the art of interface design is the art of finding the most minimal set of member functions such that all operations you might want to perform on the object can be implemented reasonably efficiently in terms of them. And this set should not support operations that shouldn't be performed on the class. So you can't just implement a bunch of getter and setter functions and call it good.

Orvas answered 13/5, 2011 at 9:36 Comment(3)
+1 for "should not support operations that shouldn't be performed"Crenellate
I would like to point out that not everybody agrees that "finding the most minimal set of member functions such that all operations you might want to perform on the object can be implemented reasonably efficiently in terms of them" is or should be a goal. Libraries of many other OO languages do not even try to achieve this. A good argument can be made that operations that are exclusively or primarily associated with a single instance of a given class should be members of it, since for instance this allows the implementations ('who calls who?') to vary over time without affecting client code.Resuscitator
My experience has tended to be that standard library designers often think that having a minimal interface to classes is a great idea, since it saves them work, but that library USERS are often very frustrated by libraries designed this way. (Consider the endless people asking "why isn't there a std::string::contains method like other languages have?" for instance? Or std::set::contains?). Also, having lots of nonmember functions with common names can clutter the global namespaces and create unexpected collisions and strange behavior when used with templates.Resuscitator
H
12

I think the reason for this rule is that by using member functions you may rely too much on the internals of a class by accident. Changing the state of a class is not a problem. The real problem is the amount of code you need to change if you modify some private property inside your class. Keeping the interface of the class (public methods) as small as possible reduces both the amount of work you will need to do in such a case and the risk of doing something weird with your private data, leaving you with an instance in an inconsistent state.

AtoMerZ is also right, non-member non-friend functions can be templated and reused for other types as well.

By the way you should buy your copy of Effective C++, it's a great book, but do not try to always comply with every item of this book. Object Oriented Design both good practices (from books, etc.) AND experience (I think it's also written in Effective C++ somewhere).

Hallam answered 13/5, 2011 at 9:26 Comment(1)
and don't always follow Object Oriented Design guidelines in C++, it's multi-paradigm, so some things are better expressed otherwise.Berserk
C
7

Various thoughts:

  • It's nice when non-members work through the class's public API, as it reduces the amount of code that:
    • needs to be carefully monitored to ensure class invariants,
    • needs to be changed if the object's implementation is redesigned.
  • When that isn't good enough, a non-member can still be made a friend.
  • Writing a non-member function is usually a smidgeon less convenient, as members aren't implicitly in scope, BUT if you consider program evolution:
    • Once a non-member function exists and it is realised that the same functionality would be useful for other types, it's generally very easy to convert the function to a template and have it available not just for both types, but for arbitrary future types too. Put another way, non-member templates allow even more flexible algorithm reuse than run-time polymorphism / virtual dispatch: templates allow something known as duck typing.
    • An existing type sporting a useful member function encourages cut-and-paste to the other types that would like analogous behaviour because most ways of converting the function for re-use require that every implicit member access be made an explicit access on a particular object, which is going to be a more tedius 30+ seconds for the programmer....
  • Member functions allow the object.function(x, y, z) notation, which IMHO is very convenient, expressive and intuitive. They also work better with discovery/completion features in many IDE's.
  • A separation as member and non-member functions can help communicate the essential nature of the class, it's invariants and fundamental operations, and logically group the add-on and possibly ad-hoc "convenience" features. Consider Tony Hoare's wisdom:

    "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult."

    • Here, non-member usage isn't necessarily far more difficult, but you do have to think more about how you're accessing member data and private/protected methods and why, and which operations are fundamental. Such soul searching would improve the design with member functions too, it's just easier to be lazy about :-/.
  • As non-member functionality expands in sophistication or picks up additional dependencies, the functions can be moved into separate headers and implementation files, even libraries, so users of the core functionality only "pay" for using the parts they want.

(Omnifarious's answer is a must-read, thrice if it's new to you.)

Crenellate answered 26/5, 2011 at 8:59 Comment(0)
E
4

The motivation is simple: maintain a consistent syntax. As the class evolves or is used, various non-member convenience functions will appear; you don't want to modify the class interface to add something like toUpper to a string class, for example. (In the case of std::string, of course, you can't.) Scott's worry is that when this happens, you end up with inconsistent syntax:

s.insert( "abc" );
toUpper( s );

By only using free functions, declaring them friend as needed, all functions have the same syntax. The alternative would be to modify the class definition each time you add a convenience function.

I'm not entirely convinced. If a class is well designed, it has a basic functionality, it's clear to the user which functions are part of that basic functionality, and which are additional convenience functions (if any such exist). Globally, string is sort of a special case, because it is designed to be used to solve many different problems; I can't imagine this being the case for many classes.

Emarie answered 13/5, 2011 at 9:42 Comment(5)
Could you rephrase "As the class evolves or is used, various non-member convenience functions will appear; you don't want to modify the class interface to add something like toUpper to a string class, for example. (In the case of std::string, of course, you can't.) Scott's worry is that when this happens, you end up with inconsistent syntax:" toUpper seems to like a member, making that a convenience function is not right, correct?Tymothy
@Umut Yes. By convenience function, I more or less meant any function that was added later, which didn't require access to the class' private members. The issue is just to allow such additional functions to use the same calling syntax, so that a later user didn't have to distinguish what was added, and what was original.Emarie
what do you mean by same calling syntaxTymothy
@Umut Tabak That the same syntax is used to call both functions.Emarie
Scott said to prefer non-member non-friend functions - not to make all functions non-members, even those that need private/friend access rights. He didn't say to prefer friends over members, for consistent calling syntax nor for any other reason.Shuman
S
4

So, first question, should not they be members than?

No, this doesn't follow. In idiomatic C++ class design (at least, in the idioms used in Effective C++), non-member non-friend functions extend the class interface. They can be considered part of the public API for the class, despite the fact that they don't need and don't have private access to the class. If this design is "not OOP" by some definition of OOP then, OK, idiomatic C++ is not OOP by that definition.

stretch the same reasoning to some other functions in vector class

That's true, there are some member functions of standard containers that could have been free functions. For example vector::push_back is defined in terms of insert, and certainly could be implemented without private access to the class. In that case, though, push_back is part of an abstract concept, the BackInsertionSequence, that vector implements. Such generic concepts cut across the design of particular classes, so if you're designing or implementing your own generic concepts that might influence where you put functions.

Certainly there are parts of the standard that arguably should have been different, for example std::string has way too many member functions. But what's done is done, and these classes were designed before people really settled down into what we now might call modern C++ style. The class works either way, so there's only so much practical benefit you can ever get from worrying about the difference.

Superposition answered 13/5, 2011 at 10:31 Comment(0)
S
2

I think sort is not implemented as a member function because it's widely used, not only for vectors. If they had it as a member function, they'd have to re-implement it each time for each container using it. So I think it's for easier implementation.

Smarten answered 13/5, 2011 at 9:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.