views:

472

answers:

8

For some time I've been designing my class interfaces to be minimal, preferring namespace-wrapped non-member functions over member functions. Essentially following Scott Meyer's advice in the article How Non-Member Functions Improve Encapsulation.

I've been doing this with good effect in a few small scale projects, but I'm wondering how well it works on a larger scale. Are there any large, well regarded open-source C++ projects that I can take a look at and perhaps reference where this advice is strongly followed?

Update: Thanks for all the input, but I'm not really interested in opinion so much as finding out how well it works in practice on a larger scale. Nick's answer is closest in this regard, but I'd like to be able to see the code. Any sort of detailed description of practical experiences (positives, negatives, practical considerations, etc) would be acceptable as well.

A: 

One of the main reasons to use C++ is the availability of classes. If you've got a set of functions like:

Molecule FormBond(const Ion& a, const Ion& b);
Molecule FormBond(const std::vector<Ion>& ions);
bool DoesMoleculeReactWithChemical(const Molecule& mol, const IChemical& chem);

It usually makes more sense to make those declarations members of Molecule rather than non-member functions:

class Molecule : public IChemical {
    public: Molecule(const Ion& a, const Ion& b);
    public: Molecule(const std::vector<Ion>& ions);
    public: bool reactsWithChemical(const IChemical& chem);
    // ...
};

In other words, if Scott Meyer's advice is to avoid member functions, then Scott Meyer is wrong. (In fact, his article states he prefers friend declarations over member functions, which is a huge red flag.) I don't know the man and he's probably not on SO to defend himself, but in this particular case, I would not take his advice.

There are certainly functions, especially algebraic "pure" functions, that don't belong inside a class, but for functions that are clearly associated with a class, there is neither a stylistic nor a performance benefit from making them non-member functions.

Jonathan Grynspan
Taking your example, why do you have `Molecule::reactsWithChemical()` and not `IChemical::reactsWithMolecule()` ? In C++, class interface is not just member functions.
Cubbi
So you can get to know Scott Meyers (http://www.aristeia.com/)
Harald Scheirich
@Cubbi: Who says I don't have both? I was showing a possible interface, not a concrete example. In this case, of course, I can handwave it by saying `IChemical::reactsWithChemical()` is defined as `virtual = 0` and that `Molecule` is implementing it, but that's also not germane to the original question, I don't think.
Jonathan Grynspan
He only prefers friend over members for operator>>,<< and when you need conversion on the lhs.
Lou Franco
Actually, friend declarations *can* indeed enhance encapsulation if used at the right time. This is not valid for me by advice of Meyers (who is one of the few renowned C++ experts by the way), but by my own experience. I pretty much agree to what is written here FAQ: http://www.parashift.com/c++-faq-lite/friends.html#faq-14.2 for what it's worth.
Jim Brissom
Obviously, people strongly disagree with my feelings about `friend` declarations. I don't want to start a flame war on an issue like this, so I defer to the masses.
Jonathan Grynspan
@Jonathan Grynspan: I think it's one of the cases where nonmember is preferred, even without considering generic programming concepts (where pretty much everything is a nonmember). Reactivity is equally coupled with either argument and binding it to one of them breaks the idea that OOP imitates physical world.
Cubbi
I understand what you're saying, but I respectfully disagree. Just because an operation is commutative does not mean it should not be strongly coupled to a type. Equality operators are, if well-formed, commutative, yet we are perfectly comfortable writing them as members. It is the *underlying reason* for that commutativity that is shared (in the case of molecules, the laws of physics), and that we may often wish to decouple from a particular class, but the class should still maintain a public interface for it.
Jonathan Grynspan
@Jonathan: I'm not well at all writing comparison as members. In fact, I always recommend writing them as free functions. Re "One of the main reasons to use C++ is the availability of classes": For me the main reason for using C++ is that it is a __multi-paradigm language__. Usually __C++ shines brightest were paradigms are mixed__. OO is by no means a holy grail. Just compare your average run-off-the-mill OO container/algorithm lib to the STL's raw power, achieved by abandoning OO.
sbi
@Jonathan: why would you *want* strong coupling if it isn't necessary? And where's the benefit in making those operations class members? I'm not "perfectly comfortable" writing equality operators as members *if it can be avoided*. If they can be implemented as non-members, they have no business being in the class.
jalf
@jalf: Everything can be implemented as non-members. You can reimplement the entire STL using non-members. Hell--you can reimplement the whole thing in Brainf___ if you so choose. That something *can* be done without classes is not an argument that it *should* be done without classes. My original point, which seems to have been overlooked, is not that everything *must* be a member of a class; rather, it is that things should be made members of classes when they are strongly associated with those classes. Why this is such a horrifying concept to some folks here, I don't know.
Jonathan Grynspan
@Jonathan: I never said doing anything without classes. I said that *if a function doesn't need access to private members of a class, it shouldn't be a member*. And it is a horrifying concept because you're creating big bloated god-classes *for absolutely no benefit*. Because you're violating encapsulation and making your code harder to maintain. It is a horrifying concept because you're choosing 15-year old OOP dogma over objectively sound design principles. Any OOP newbie knows that encapsulation is good, coupling is bad. So why would you add stronger coupling and less encapsulation?
jalf
You are violating the *goals* of OOP in order to satisfy some misunderstood OOP dogma people came up with years ago to make it look like Java was an OOP language.
jalf
@jalf: I'm sorry, but I never mentioned Java, and I'm not pushing dogma. Clearly you feel very strongly on the matter, but you're making incorrect assumptions about me and my position here.
Jonathan Grynspan
@Jonathan: you never mentioned Java, but you are pushing dogma that was popularized in Java. Where you got it from is irrelevant, the point is that it's a collection of bad practices which serve no purpose other than to make languages such as Java look as if they're "proper" OOP (which they're not, if you ask the person who invented OOP). And yes, I do feel strongly about intentionally making your code *less* maintainable for no benefit. You won't get me to back down there, I'm afraid ;)
jalf
@jalf: Not asking you to. We're coming at OOP from different directions, one assumes, but in the end neither of us is going to build a mountain of spaghetti code with our preferred techniques, so in the end, it's not really worth this hubbub over.
Jonathan Grynspan
I disagree, but it won't be *my* spaghetti code, so I'll leave it. I think I've made my point. ;) Btw, Scott Meyer is one of the foremost C++ gurus. And he's backed up on this point by Herb Sutter, *another* C++ guru (and chairman of the C++ standardization committee)
jalf
Whoa! I tend to agree with Jonathan, *and* with Herb. They are not in conflict. It's only over-simplified caricatures of their viewpoints that might seem to be in conflict. For what it's worth I don't agree with Scott Meyers on this matter; as I see it he's over-generalizing here, or he was. Anyway, I think it's really Bad Practice to score down a valid answer that one emotionally feels uncomfortable with or disagrees with; rather, it should be scored up, because it provides useful information.
Alf P. Steinbach
A: 

As stated in the article, STL has both member and non-member functions. This isn't because of his preference -- it's mostly because many of the free functions operate on iterators and iterators are not in a class hierarchy (because STL wants pointers on arrays to be first-class iterators).

I strongly disagree with this article for C++, because the inconsistency would be annoying, and in modern IDE's intellisense would be broken.

In C#, however, with extension methods, this is all very good advice. There, you get the best of both worlds -- you can make non-member functions that can appear to be member functions. They also can be in separate files -- getting all of the benefits of this practice without the inconsistency drawback.

Lou Franco
What inconsistency? If you absolutely need consistency, just wrap the few remaining member functions in non-member versions, then they all have the same (non-member) syntax. And I honestly don't think the convenience of intellisense (which usually doesn't work in C++ *anyway*) is more important than actually writing well structured code. I, for one, prefer encapsulation over intellisense. :)
jalf
I don't know about Intellisense, but most IDE's are smart about finding all overloads when you do "go to definition."
Potatoswatter
+5  A: 

I'd argue that the benefit of non-member functions increases as the size of the project increases. The standard library containers, iterators, and algorithms library are proof of this.

If you can decouple algorithms from data structures (or, to phrase it another way, if you can decouple what you do with objects from how their internal state is manipulated), you can decrease coupling between your classes and take greater advantage of generic code.

Scott Meyers isn't the only author who has argued in favor of this principle; Herb Sutter has too, especially in Monoliths Unstrung, which ends with the guideline:

Where possible, prefer writing functions as nonmember nonfriends.

I think one of the best examples of an unneccessary member function from that article is std::basic_string::find; there is no reason for it to exist, really, as std::find provides exactly the same functionality.

James McNellis
+1: I like this answer. (http://stackoverflow.com/questions/2994073/why-there-is-no-find-for-vector-in-c/3744732#3744732)
Chubsdad
Although I don't like that the string class has so many methods, there is a reason for `std::basic_string::*` methods to exist: they forward requests to `std::char_traits<>` which uses possibly more optimized string operations, such as those of your CISC processor, or a really fast version of `strlen()`.
André Caron
What needs to happen is `std::find` needs to be partially specialized on string types to use `std::char_traits<>` instead of the vanilla `std::find`. All the optimization benefits but still a solid design.
caspin
+6  A: 

I do this quite a bit on the project I work on; the largest of which at my current company is around 2M lines, but it's not open source, so I can't provide it as a reference. However, I will say that I agree with the advice, generally speaking. The more you can separate the functionality which is not strictly contained to just one object from that object, the better your design will be.

By way of an example, consider the classic polymorphism example: a Shape base class with subclasses, and a virtual Draw() function. In the real world, Draw() would need to take some drawing context, and potentially be aware of the state of other things being drawn, or the application in general. Once you put all that into each subclass implementation of Draw(), you're likely to have some code overlap, or most of your actual Draw() logic will be in the base class, or somewhere else. Then consider that if you want to re-use some of that code, you'll need to provide more entry points into the interface, and possibly pollute the functions with other code not related to drawing shapes (eg: multi-shape drawing correlation logic). Before long, it'll be a mess, and you'll wish you had a draw function which took a Shape (and context, and other data) instead, and Shape just had functions/data which were entirely encapsulated and not using or referencing external objects.

Anyway, that's my experience/advice, for what it's worth.

Nick
+1. Simply put, avoiding molding functions into class interfaces removes a possible impediment to factorization.
Potatoswatter
+2  A: 

One practical advantage of writing functions as nonmember nonfriends is that doing so can significantly reduce the time it takes to thoroughly test and verify the code.

Consider, for example, the sequence container member functions insert and push_back. There are at least two approaches to implementing push_back:

  1. It can simply call insert (it's behavior is defined in terms of insert anyway)
  2. It can do all the work that insert would do (possibly calling private helper functions) without actually calling insert

Obviously, when implementing a sequence container, you probably want to use the first approach. push_back is just a special form of insert and (to the best of my knowledge) you can't really get any performance benefit by implementing push_back some other way (at least not for list, deque, or vector).

However, to thoroughly test such a container, you have to test push_back separately: since push_back is a member function, it can modify any and all of the internal state of the container. From a testing standpoint, you should (must?) assume that push_back is implemented using the second approach because it is possible that it could be implemented using the second approach. There is no guarantee that it is implemented in terms of insert.

If push_back is implemented as a nonmember nonfriend, it can't touch any of the internal state of the container; it must use the first approach. When you write tests for it, you know that it can't break the internal state of the container (assuming the actual container member functions are implemented correctly). You can use that knowledge to significantly reduce the number of tests that you need to write to fully exercise the code.

James McNellis
A: 

(I don't have time to write this up nicely, the following's a 5 minute brain dump which doubtless can be ripped apart at various trival levels, but please address the concepts and general thrust.)

I have considerable sympathy for the position taken by Jonathan Grynspan, but want to say a bit more about it than can reasonably be done in comments.

First - a "well said" to Alf Steinbach, who chipped in with "It's only over-simplified caricatures of their viewpoints that might seem to be in conflict. For what it's worth I don't agree with Scott Meyers on this matter; as I see it he's over-generalizing here, or he was."

Scott, Herb etc. were making these points when few people understood the trade-offs or alternatives, and they did so with disproportionate strength. Some nagging hassles people had during evolution of code were analysed and a new design approach addressing those issues was rationally derived. Let's return to the question of whether there were downsides later, but first - worth saying that the pain in question was typically small and infrequent: non-member functions are just one small aspect of designing reusable code, and in enterprise scale systems I've worked on simply writing the same kind of code you'd have put into a member function as a non-member is rarely enough to make the non-members reusable. It's pretty rare for them to even express algorithms that are both complex enough to be worth reusing and yet not tightly bound to the specific of the class they were designed for, that being weird enough that it's practically inconceivable some other class will happen along supporting the same operations and semantics. Often, you also need to template arguments, or introduce a base class to abstract the set of operations required. Both have significant implications in terms of performance, being inline vs out-of-line, client-code recompilation.

That said, there's often less code changes and impact study required when changing implementation if operations have been implementing in terms of a public interface, and being a non-friend non-member systematically enforces that. Occasionally though, it makes the initial implementation more verbose or in some other way less desirable and maintainble.

But, as a litmus test - how many of these non-member functions sit in the same header as the only class for which they're currently applicable? How many want to abstract their arguments via templates (which means inlining, compilation dependencies) or base classes (virtual function overheads) to allow reuse? Both discourage people from seeing them as reusable, but when not the case, the operations available on a class are delocalised, which can frustrate developers perception of a system: the develop often has to work out for themselves the rather disappointing fact that - "oh - that will only work for class X".

Bottom line: most member functions aren't potentially reusable. Much corporate code isn't broken into clean algorithm versus data with potential for reuse of the former. That kind of division just isn't required or useful or conceivably useful 20 years down the road. It's much the same as get/set methods - they're needed at certain API boundaries, but can constitute needless verbosity when ownership and use of the code is localised.

Personally, I don't have an all or nothing approach to this, but decide what to make a member function or non-member based on whether there's any likely benefit to either, potential reusability versus locality of interface.

Tony
+1  A: 

OpenCV library does this. They have a cv::Mat class that presents a 3D matrix (or images). Then they have all the other functions in the cv namespace.

OpenCV library is huge and is widely regarded in its field.

Dat Chu
+1  A: 

I also do this alot, where it seems to make sense, and it causes absolutely no problems with scaling. (although my current project is only 40000 LOC) In fact, I think it makes the code more scalable - it slims down classes, reduces dependencies. It sometimes requires you to refactor your functions to make them independent of members of the class - and thereby often creating a library of more general helper functions, which you can easly reuse elsewhere. I'd also mention that one of the common problems with many large projects is the bloating of classes - and I think preferring non-member, non-friend functions also helps here.

Cornelius Scarabeus