C++ class design: member function vs nonmember function
I was recently re-reading Effective C++ by Scott Meyers and I noticed one item that seems very strange to me upon examination:
Item 23: Prefer non-member non-friend functions to member functions.
This basically says that if you need to add a function that works on a class, it is better to make this function a non-member function than a member function. The reasoning behind this seems to be that if you used a member function, then you are not following the intent behind encapsulation. That is, a new member function will increase the amount of code that has access to the private variables of the class it belongs to where as a non-member function does not.
This seemed to make sense at first, but if you take this item to the extreme, it seems to suggest that the best class design would be to make every class just have setter and getters for their private variables and then have any method that works on the class as a non-member function... which to me seems wrong.
I was wondering what other people's take on this is. Do you just rely on gut instinct to make your decision?
I originally started thinking of this more because I was looking at some math libraries and some libraries seem to prefer making the "cross product" function a member function of a "vector" class while others made it a nonmember function. Of course this is a trivial case, but this decision seems to come up quite a lot.
If all your classes are nothing but collections of data, you're probably doing something wrong. If you're making plain old getters/setters that go to a single field, you're doing something wrong.
My take is that it's mostly accurate. It's easier to extend class behavior with free functions, and it tends to result in cleaner class interaction when the core behavior is part of the class and the 'helper' behavior is not.
My take is that it's mostly accurate. It's easier to extend class behavior with free functions, and it tends to result in cleaner class interaction when the core behavior is part of the class and the 'helper' behavior is not.
A vector math library is a possible counter-example for it.
Remember that it is a guideline, and generally it is correct to prefer the least coupled interface.
The nature of the problem is that you need access to certain aspects as raw data. But there are other elements that want encapsulation.
As a single example, consider a Normalize() function. As a member, it would allow you to have internal hidden data, such as caching if it has been normalized. As a freestanding function, it allows you to operate on anything that implements the interface. Both have their own benefits.
Vector math libraries are not typical of all programming tasks.
As a rule, you really should prefer having a collection of "verb" functions rather than just accessors and mutators.
Remember that it is a guideline, and generally it is correct to prefer the least coupled interface.
The nature of the problem is that you need access to certain aspects as raw data. But there are other elements that want encapsulation.
As a single example, consider a Normalize() function. As a member, it would allow you to have internal hidden data, such as caching if it has been normalized. As a freestanding function, it allows you to operate on anything that implements the interface. Both have their own benefits.
Vector math libraries are not typical of all programming tasks.
As a rule, you really should prefer having a collection of "verb" functions rather than just accessors and mutators.
Quote:
This seemed to make sense at first, but if you take this item to the extreme, it seems to suggest that the best class design would be to make every class just have setter and getters for their private variables and then have any method that works on the class as a non-member function... which to me seems wrong
Don't take it to the extreme.
The member functions should be the minimal set of behaviour the object needs to work. Functions that can be implemented in terms of these should be non-member functions.
No member functions should expose data in such a way that the class invariants cannot be maintained, so get/set pairs are typically indicative of a class with broken encapsulation - or a class with no invariants. Either way it hints that the design of the class might be flawed.
Math vectors are typically PODs with no invariants, so most of the reasons for preferring non-member functions don't apply. I'd make them non-member functions for consistency; though I like to have the length and lengthSqrd as member functions. I think of the length as a property rather than a function, but C++ does not directy support derived properties.
Quote: Original post by Taralieth
This seemed to make sense at first, but if you take this item to the extreme, it seems to suggest that the best class design would be to make every class just have setter and getters for their private variables and then have any method that works on the class as a non-member function... which to me seems wrong.
Not wrong, just C [grin]. Basically, if you switch out the getters and setters for public variables, you have procedural programming in C *. And there's nothing really wrong with that. Basically, he's just saying that as long as you're working in a multi-paradigm language, take the best from each world.
*Well, not exactly. But you know what I mean.
Quote: Original post by Taralieth
it seems to suggest that the best class design would be to make every class just have setter and getters for their private variables and then have any method that works on the class as a non-member function... which to me seems wrong.
How so? If my class as 40 data members and 5 possible actions then getters and setters make 80 functions vs 5 in the well designed interface.(As long as we are considering the absurd route any way.)
The idea is that you want the fewest number of functions to be privy to the class's internals to minimize the effect of change in the class. In the getters and setters instance you haven't reduced the amount of code privy to the class's internals. You want a logically small interface(in terms of privileged knowledge) not the one with the smallest head count.
The rule of thumb I try to apply is that a function related to a class which modifies the class instance are always member functions (usually necessarily,) and functions which take no parameters in addition to the implicit 'this' are also implimented as members unless they can be plainly and efficiently implimented in terms of other public functionality.
This leads to a mirror image of the popular idiom of defining a binary operator in terms of its self-modifying equivilent:
I'm sure I've not always adhered to these rules perfectly, and implimenting functionality as non-member friend functions is never preferable to a member function, but I find that this usually leads to a fairly minimal API suitable for building higher-level functionality as non-member, non-friend functions.
I think of this suggestion as the class-design equivilent to the guiding force of the design of C++ itself. One of the core design goals of C++ is to keep the core of the language small, by only extended the core language with features that either *require* compiler-level support, or features that enable library designers to make more efficient, more maintainable libraries -- this is why std::string isn't a built-in type, as it is in many languages: The core language makes implimenting strings as a class efficient, and making it a class means you can rip it out and replace it if you really wanted to. In terms of classes, this is the same idea as only making member functions that *require* direct access to class internals, or which enable efficient higher-level constructs to be built on through external means.
However, you always hold the trump card of "because I feel like it" which allows you to impliment whatever you want as a member function if it feels 'icky' to write it as a non-member.
This leads to a mirror image of the popular idiom of defining a binary operator in terms of its self-modifying equivilent:
class vector2{ //... vector2 & operator+=(const vector2 &rhs) { X += rhs.X; Y += rhs.Y; return *this; } vector2 & normalize() { float length = length(); X /= length; Y /= length; return *this; }};vector2 operator+(vector2 lhs, const vector2 &rhs){ return lhs += rhs;}vector2 normal(vector2 lhs){ return lhs.normalize();}
I'm sure I've not always adhered to these rules perfectly, and implimenting functionality as non-member friend functions is never preferable to a member function, but I find that this usually leads to a fairly minimal API suitable for building higher-level functionality as non-member, non-friend functions.
I think of this suggestion as the class-design equivilent to the guiding force of the design of C++ itself. One of the core design goals of C++ is to keep the core of the language small, by only extended the core language with features that either *require* compiler-level support, or features that enable library designers to make more efficient, more maintainable libraries -- this is why std::string isn't a built-in type, as it is in many languages: The core language makes implimenting strings as a class efficient, and making it a class means you can rip it out and replace it if you really wanted to. In terms of classes, this is the same idea as only making member functions that *require* direct access to class internals, or which enable efficient higher-level constructs to be built on through external means.
However, you always hold the trump card of "because I feel like it" which allows you to impliment whatever you want as a member function if it feels 'icky' to write it as a non-member.
throw table_exception("(? ???)? ? ???");
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement
Recommended Tutorials
Advertisement