tags:

views:

459

answers:

8

I have a colleague in my company who's opinions I have a great deal of respect for, but I simply cannot understand one of his preferred styles of writing code in C++.

For example, given there is some class A, he'll write global functions of the type:

void foo( A *ptrToA ){}

or:

void bar( const A &refToA ){}

My first instinct upon seeing global functions like that is: "Why aren't these members of A?" He'll insist up and down that this is consistent with recommendations for good practice in C++, because foo and bar can perform all they need to perform by using the public interface of A. For example, he'll argue that this is completely consistent with Scott Meyers Effective C++ recommendations. I find it hard to reconcile this with item 19 in that book which basically says everything should be a member function with a few exceptions (operator<< and operator>> and functions that need dynamic type conversion). Furthermore, while I agree that the functions can do what they need to do with the public interface of A, in my opinion, that's largely the result of people writing classes that have getters and setters for every data member of class A. So with that public interface, A is an over-glorified struct and you certainly can do anything with the public interface. Personally, I don't think that should be exploited, I think it should be discouraged.

Obviously, this is only possible in a language like C++ that is not pure object oriented, so I guess one way of looking at it is that my colleague does not favor a pure object oriented approach to software design. Does anyone know of any literature that supports this position as a best practice? Or does anyone agree with this and can possibly explain it to me in a different way than my colleague has so that I might see the light? Or does everyone agree with my current feeling that this just doesn't make much sense?

Edit: Let me give a better code example.

class Car
{
    Wheel frontLeft;
    Wheel frontRight;
    Wheel rearLeft;
    Wheel rearRight;
    Wheel spareInTrunk;

public:
    void wheelsOnCar( list< Wheel > &wheels )
    {
        wheels.push_back( frontLeft );
        wheels.push_back( frontRight);
        wheels.push_back( rearLeft);
        wheels.push_back( rearRight);
    }
    const Wheel & getSpare(){ return spareInTrunk; }
    void setSpare( const Wheel &newSpare ){ spareInTrunk = newSpare; }
    // There are getters and setters for the other wheels too,
    //but they aren't important for this example
};

Then I'll see a function like this:

void wheelsRelatedToCar( Car *aCar, list< Wheel > &wheels )
{
    aCar->wheelsOnCar( wheels );
    wheels.push_back( aCar->getSpare() );
}

This is a real example with the names of the classes and functions changed of course. Why would one want wheelsRelatedToCar to not be a member function of Car? In this real example, Car and Wheel were in the same library. The global function was defined in a source file in a specific application using that library, so the argument was made that the function was specific to the application. My response was that it was a perfectly legitimate operation on the Car and belonged with the Car class. Is there another perspective to look at it (other than one who does not prefer to use object oriented design)?

+12  A: 

Scott Meyers has advocated that non-member functions often improve encapsulation:

Herb Sutter and Jim Hyslop also talk about this (citing Meyer's article) in "Self-Sufficient Headers"

These ideas have been republished (in more refined form) in the 3rd edition of Meyer's "Effective C++", "Item 23: Prefer non-member non-friend functions to member functions ", and Sutter/Alexandrescu's "C++ Coding Standards", "44 - Prefer writing nonmember nonfriend functions".

I think a lot of developers find this non-intuitive and maybe a little controversial.

Michael Burr
Scott Meyers does not suggest making lots of getters and setters to improve Encapsulation through non-member functions. Does he?
jmucchiello
Getters and setters are, in general, evil, although they're better than public data members.
David Thornley
Even though an example about setting the components of a Point class is used int he article, it's not about getters/setters - it's just a simple class with an easy to understand interface to use to get the point across (::rimshot::).
Michael Burr
Yes, but by showing the example full of getters and setters and then declaring from on high that you "improve" encapsulation through non-member functions, perhaps some people get the wrong message from the article.
jmucchiello
I'm not vilifying Meyers for slap-dash choice of example. I saying perhaps in this case he should have taken more care. He understands inherently that a class' public interface should be streamlined and minimal. That is in fact what the article is really about. But he focused on how great non-member functions are to the extent that he forgot that not every C++ programmer knows the first part: streamline the public interface first. There's a place for focus and place for reinforcing the basics. The Questioner's "guru" friend apparently missed the forest for the article's trees.
jmucchiello
Unless I'm missing something, the Point class is the only one that involves getters/setters. The other examples do not. Even so, it's quite common for tutorial articles to use simple, otherwise not-great techniques in order to keep focus on what's being taught. How many times do you read about example code not performing proper error handling in the interest of not distracting from the intended focus? While this may not be ideal in a purist sense, it's a reasonable thing to do in many cases.
Michael Burr
I agree that the article isn't a full exposition of the topic of class design, and I'm sure Meyers would agree with you on that point. He'd probably tell you to read the book for a bit more depth (though I'm pretty sure he'd say the book also isn't really a full study on class-design). However, I think the article gets the point across about the potential usefulness of a design using nonmember functions, and the article is online while the book chapters (for either Meyers or Sutter/Alexandrescu) aren't as far as I know.
Michael Burr
I prefer my explanation. You are saying they didn't address design concerns because of space. That's bad. I'm saying they didn't address design concerns because it didn't occur to them that anyone would take their article and use it to make wide open classes to __facilitate__ the non-member function paradigm they were espousing.
jmucchiello
I think this is the best answer because I think this article is exactly where my friend got this idea embedded in his head. However, I think I agree with the comment above that he also missed the forest for the trees. I think in my more detailed example above, if the class did not have a polluted public interface to begin with, the question would be moot, because he would have had to make his wheelsRelatedToCar() function a member function.
Mutmansky
It's usually difficult to account for specifities, many classes are just glorified structs in the end. However I would distinguish such a class from a "blob". The blob consists in putting together elements that have little relation, while it's quite reasonable to group the 5 wheels of a car in the same object. In the end, I have always distinguish 2 kinds of classes: the ones with a clear responsability (handling a resource for example), and those which are in fact aggregates of related objects.
Matthieu M.
+9  A: 

You answer your own question. If the global functions can operate only using the sleek, streamlined, and unbloated public interface to the class, then they should be global. If the class has been morphed to make these functions possible, then it is not so good.

jmucchiello
+18  A: 

Herb Sutter & Andrei Alexandrescu recommandation about that:

Avoid membership fees: Where possible, prefer making functions nonmember nonfriends.


Non-member nonfriend functions:

  • improve encapsulation by minimizing dependencies: The body of the function cannot come to depend on the nonpublic members of the class.
  • reduce coupling, by breaking apart monolithic classes to liberate separable functionality
  • improve genericity, because it's hard to write templates that don't know whether or not an operation is a member for a given type.


Now, to answer your question (when ?), here is an algorithm to determine whether a function should be a member and/or friend:

If the function is one of the operators =, ->, [], or (), which must be members:
=> Make it a member

Else if: 
    a) the function needs a different type as its left-hand argument (as do stream operators, for example); 
 or b) it needs type conversions on its leftmost argument; 
 or c) it can be implemented using the class's public interface alone:
=> Make it a nonmember (and friend if needed in cases a) and b) )

If it needs to behave virtually:
=> Add a virtual member function to provide the virtual behavior, and implement the nonmember in terms of that

Else: 
=> Make it a member


References:

  • H. Sutter and Andrei Alexandrescu . C++ Coding Standards (Addison-Wesley, 2004)
  • S. Meyers . "How Non-Member Functions Improve Encapsulation" (C/C++ Users Journal, 18(2), February 2000)
  • B.Stroustrup . The C++ Programming Language (Special 3rdEdition) (Addison-Wesley, 2000). §10.3.2, §11.3.2, §11.3.5, §11.5.2, §21.2.3.1
  • H. Sutter . Exceptional C++ (Addison-Wesley, 2000).
  • H. Sutter . Exceptional C++ Style (Addison-Wesley, 2004).
Samuel_xL
That presupposes that your class is not just class Foo { public: int i; int j; void foo(); int etc(); };
jmucchiello
+12  A: 

OK

  1. Your mate is correct, if a method doesn't need to be a member of a class, then - strictly speaking - it should not be a member of a class.
  2. You are correct, exposing every item on a class, so you can write global functions, is wrong.

Your mate is correct because it's easier to build generic methods if they're not part of a class (e.g. Why is std::find not a member of std::vector? because then it wouldn't work for lists, maps etc).

You are also correct, because exposing too much to do this stuff breaks encapsulation.

When you get out into the real world and start writing apps, and working to business deadlines, you will find that every application is a competing set of requirements. "We need to get A, B, & C in, but by the end of next month. That's, impossible, they can have B & C, or A & C, but not A & B. Or they can have a not-ideal version of A & B and a good version of C".

Writing code is no different, there are plenty of laws and rules that define ideal levels of encapsulation, genericity, cohesion etc, but many of them are contradictory, and you spend so much time trying to satisfy them all that you get nothing done.

I've always said that these principals and "laws" are actually just guide lines, follow them where you can, find your own level where they sit well with you . . . and expect those levels to change every 6 months or so :)

Hope this helps.

Binary Worrier
+3  A: 

One way to look at this is this:

  • If a function manipulates an object's inner state, then that's a good indication that this function should probably be a member function.
  • If a function uses an object without changing its inner state, then that's a good indication that this function probably should be a free function.

However, this doesn't mean that it is a good idea to exclusively follow these guidelines in all cases. There are other considerations, too. For example, as has been quoted by others, non-member function contrary to popular belief often increase encapsulation. (Of course, if they deal with an object's state by means of getters/setters to private data, then that's more than questionable. In fact, I find getters/setters questionable anyway. See this excellent article on this topic.)

sbi
A: 

Part of the decision to make something a member or non-member also has to do with complexity and managing complexity.

For example, if there are 20 or fewer functions that need to be associated with a class, then why not make them all member functions.

However, when the number runs to 100 or several hundred, it often helps to organize those functions into groups and create a pseudo-class that either stores an instance variable of the other class, or serves as a holder for class/static functions that operate on the other class.

For example, one might have a document object, but an editor class that operates on it and contains the editing functions, rather than having all the editing functions on the document.

There are no hard or fast rules - just principles, such as encapsulate, manage complexity, reduce dependencies. Some of these involve tradeoffs, and have to be analyzed in the context of the problem to which they are being applied.

Larry Watanabe
pseudo-class=namespace
Sergey Skoblikov
A: 

So you are saying that a String shouldn't have an "bool IsEmpty() const" public member just because it has "int Size() const" and IsEmpty is defined and implemented as Size() == 0?

I would disagree. For me, having extra features adds more services to the class, and this is generally good if they don't undermine its ease of understanding - which, in the general case, is not necessarily so.

Daniel Daranas
While I agree with you, your example is perfect for templatizing: template <typename T> book IsEmpty(const T } That works on all the STL containers and basic_string. It must be "better".
jmucchiello
(My apologies) And you can make it work with C strings: template<> bool IsEmpty<const char*>(const char* s) { return !s || !*s; } I'll leave int and double as an exercise for the more bored than I am.
jmucchiello
Just for comparison, here's what Herb Sutter thinks of the std::string class: http://www.gotw.ca/gotw/084.htm. On "empty" in particular, he says, "the question really comes down to whether we ought to trade away real benefits in order to follow a tradition, or to change the tradition to get real benefits". In context, he means that making it a member function did the former, whereas the latter would be better if possible.
Steve Jessop
@jmucchiello: the only unsatisfactory thing about that template is that it needs to be specialised or overloaded for hypothetical classes which have an O(n) `size()` function, but where you can implement `IsEmpty()` in O(1). Of course you can reasonably say that no such collection should ever exist, but currently the C++ standard says that `empty()` is constant complexity, whereas `size()` merely "should have constant complexity".
Steve Jessop
+1  A: 

Your colleague is right. wheelsRelatedToCar shouldn't be a member function of class Car, because this operation is not even applied (only) to the Car. This function is doing something with the Car and something else with the list of wheels.

So, it should be a member function of some other, higher level class, like class Mechanic or class WheelReplacementOperation. And in case that you don't have any such class (though I think you should have!), better make this function global.

Igor Oks
I still would have to say that I still disagree (maybe I'm just being contrary). If the class had a minimal public interface (i.e. it didn't have the getSpare() and setSpare() methods or any other getters and setters), in order to write the wheelsRelatedToCar method, one would have to add the getSpare() method to the public interface or add the wheelsRelatedToCar() method to the public interface. Personally, my instinct is if I have to add to the public interface, make it a clear, meaningful, method rather than a straight-up getter. Perhaps its just personal style...
Mutmansky
Furthermore, assuming Wheel and Car are defined in the same "level" (i.e. in the same library for example) I don't think there's any problem with the public interface of Car using a same level or lower level object (Wheel in this case) as a means of communication.
Mutmansky