开发者

When should functions be member functions?

I have a colleague in my company whose 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( rearLef开发者_C百科t);
        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)?


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

  • How Non-Member Functions Improve Encapsulation

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

  • http://www.ddj.com/cpp/184401705

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.


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


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.


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.


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


In addition to already mentioned articles, I think it is worth to quote several additional opinions from experts:


  • Alexander Stepanov. Notes on Programming:

While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built-in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee. In general, there were many compromises of which I am ashamed. It would have been harder to succeed without making them, but I still get a metallic taste in my mouth when I encounter all the things that I did wrong while knowing full how to do them right. Success, after all, is much overrated. I will be pointing to the incorrect designs in STL here and there: some were done because of political considerations, but many were mistakes caused by my inability to discern general principles.)


  • Bjarne Stroustrup. The C++ Programming Language:

10.3.2 Helper Functions

Typically, a class has a number of functions associated with it that need not be defined in the class itself because they don’t need direct access to the representation. For example:

int diff(Date a,Date b); // number of days in the range [a,b) or [b,a)
bool leapyear(int y);
Date next_weekday(Date d);
Date next_saturday(Date d);

Defining such functions in the class itself would complicate the class interface and increase the number of functions that would potentially need to be examined when a change to the representation was considered. How are such functions "associated" with class Date? Traditionally, their declarations were simply placed in the same file as the declaration of class Date, and users who needed Dates would make them all available by including the file that defined the interface.


  • Andrei Alexandrescu. Comment:

Basically my belief is that nonvirtual member functions in general are an unnecessary cutesy in C++ that wahses people's brains, leads to bad programs, and will take many years to wear off. For smart pointers in particular, they can do even more harm.


While I agree with most other answers here, that free functions should be used when possible, there's also another side of the coin that needs attention: The pragmatic programmer's view. If I'm new to a code base, the discoverability of behavior for a given type is infinitely higher if that behavior is exposed as member functions. Think intellisense. You will not get that with free functions.


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.


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.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜