Do I use a member function or a non-member function?
This question was posed in an article [1] by Scott Meyers. How you answer it depends on your perspective.
The best approach adopts a problem based perspective: What are you trying to achieve?
However, as a thought experiment, I'll take an implementation perspective. At first, I thought we were mixing two paradigms, Object Orientation (OO.) and Procedural (algorithms).
Different designers will produce different designs for the same problem. A key factor is the paradigm mix they prefer. Rephrasing the question makes it easier to answer:
"What are the design implications of member functions and non-member functions?"
The answer, ultimately depends more on your personal design style mix than the original problem. Here are two extreme examples:
If you are heavily into OO then you'll be using member functions everywhere. Even your algorithms will be wrapped up in classes and are likely to be passed around as objects. The original question becomes "which class do I make this a member function of?"
If you're heavily into procedural programming, there'll be plenty of highly-cohesive, weakly coupled functions manipulating a small set of built in types. The original question never gets asked.
Actually you are somewhere between those two artificial extremes. Everyone knows the C++ language supports multiple paradigms - procedural, modular, object-based, object-oriented and more. And language shapes thought. So, at some intuitive level, we're all doing multi-paradigm design and implementation. There comes a point where it is worth looking at what we're doing more formally. Which is a perfect excuse to introduce some formal terminology…
Multi-Paradigm Design
A paradigm is just a matter of perspective - a way of grouping similar things together and noting their differences. Some points of views are similar and overlap (object-based, object-oriented). As far as intuition is concerned, the differences do not matter.
When weaving paradigms together, I find it a helpful crutch to have artificially rigid definitions of the paradigms I'm using. The paradigms have to overlap in some way so they can be stitched together. Personally I think the different paradigms of C++ can be bundled together as families of paradigms (algorithms, user defined types, declarative).
When designing something, we make a careful study of the situation/business (domain analysis). Sometimes the business is so broad that its best split up into smaller areas. I prefer to split the business focus into sub-domains (e.g. forecasting, risk-analysis) and the technology into sub-systems (e.g. databases, GUIs).
When we split something up, we group things together into bunches ( families ) based on what they have in common ( commonality analysis ) and how the family members differ ( variability analysis ).
Table 2 (next page) describes an informal, intuitive approach to Multi-Paradigm design. At the end of this intuition-driven approach, there may be some functions that could be put in classes or could be stand-alone functions. This uncertainty shows that we will discover something later.
Once the topic/domain has been split into sub-domains and sub-systems, the choice of paradigm may be obvious to intuition.
While paradigm choice is important in selecting the tools we will use, it only brings us nearer to the answer of our question. Some email discussion with Kevlin Henney pointed out that "questions … where responsibility for behaviour is allocated .. the forces at play on this include extensibility, primitiveness, dependencies, and so on…you might want to look at …the Interface Principle".
"Does the function's behaviour belong inside the class?"
Applying design principles.
How about considering the Open Closed Principle (OCP)? That is, what we implement should be open for extension and closed to change.
Applying the OCP to the function, should the function extendable (applicable to other types)? If so then we should consider making it a template function.
According to the Interface Principle (IP) [4], regardless of whether the function is implemented as a member function or a freestanding function, the function is part of the class' interface. I will refer to the member functions as the class method interface.
So, taking the OCP and IP together, if we add a member function to the class we change it, if we write a freestanding function that mentions the class, we extend the class - without changing the class definition or using virtual functions! According to the OCP, we should provide things that are open to extension and closed to change.
For the class to be closed to change, we need to provide member functions that provide meaningful services so that non-member functions do not have to access private data.
We need to look at the function and the class regarding the principles of weak coupling and high cohesion (and consider granularity for good measure). A coupling issue arises if the function relies on private data. We then need to make a decision based on cohesion to either (1) make the function a member function or (2) write the function as a non-member function and write some additional member functions so that the private data is no longer an obstacle.
Polymorphism requirements don't affect our decision. If polymorphism is needed, the function should either be a virtual member function or a freestanding function that accepts a base class pointer/reference as a parameter.
Remember, though, whatever choice you make, it should take the application into account.
Member function implies | Non-member function implies |
Function is part of the class method interface.
|
Function is part of the class interface.
|
Table 1. Design implications of member functions and non-member functions
Ambiguity implies the function is one of:
-
A future member function of
-
A class that does not exist yet.
-
A class we do not know enough about to decide.
-
A future algorithm that may get applied to different types one day using
-
Runtime polymorphism (inheritance class hierarchy)
-
Compile time polymorphism (template function).
Examples of mixing both:
-
A template that takes base class pointers as parameters.
-
A class hierarchy in which the member functions are implemented using different, smaller function templates.
Think about the
problem.
Break the original problem (application domain) into sub-domains
& sub-systems.
|
Table 2. A development approach.
Acknowledgements
Thanks to Kevlin Henney and Phil Bass for their comments.
References & Further Reading.
[Meyers] How Non-Member Functions Improve Encapsulation (Scott Meyers, via www.aristeia.com )
[Stroustrup] The C++ Programming Language 3rd edition . (Bjarne Stroustrup). Chapter 2: Overview of paradigms and Part IV: Design Using C++.
[Coplien] Multi-Paradigm Design for C++ (James O. Coplien). Especially chapters on Solution domain analysis, Commonality, Variability.
[Sutter] Exceptional C++ (Herb Sutter)
Combining O.O. Design and Generic Programming ( Klaus Kreft & Angelika Langer, C++ Report March 1997, home.camelot.de/langer/Articles/OOPvsGP/Introduction.htm)
C++ Primer 3e (Lippman & Lajoie) chapters on Function templates, Overloaded functions.