There are things that are objects. Things that have state and change their state are objects. And then there are things that are not objects. A binary search is not an object. It is an algorithm.
Alex Stepanov
Introduction
Most of us who use object oriented languages tend to think that non-member functions are a procedural programming tool that has no place in OOP. In this article, I will try to show that non-member functions are still very useful, and that by using non-member functions you can improve design of your OO code. This is by no means a criticism of object oriented programming in general. On the contrary, my aim is to show that non-member functions can fit into OO design just fine. I believe that careful use of non-member function can:
- make the code more readable and maintainable
- improve data encapsulation
- reduce class coupling
Note that I never talk about global functions here. For anything but the very small projects, I strongly advocate use of namespaces in order to avoid global namespace pollution and name clashes.
Non-member functions in action
Contrary to what "OO-purists" would like us to believe, not everything is an object. Yes, when you have some data, and some operations on that data, it makes perfect sense to wrap them into a class. However, there are operations that deal with different data at the same time, and in that case it may be more logical to implement this operation as a non-member function.
Readability
Let's illustrate my point with an example:
class sample_string
{
std::vector<char> data_;
public:
explicit sample_string (const char* cstr);
int get_length() const;
char get_at (int index) const;
void append(const sample_string& other);
sample_string concat (const sample_string& other);
};
We are going to discuss which of the member functions should be non-members. But, before we do that, let's try to answer the ultimate question: Why did we make class sample_string
at all?. To make our program "more object oriented"? Wrong! The primary reason for making this class was to protect our data from improper usage. The secondary reason was to make string operations more logical and readable through coupling the data with operations that handle it.
Back to our sample. Our data is made private, and this is good. Now we are in charge of how the data is accessed, and this was the primary goal of making this class. Now, what about member functions?
get_length
without any doubt belongs to the class. It retrieves information about object's state, and to do so, it needs read-only access to class internal implementation. All of that can be concluded by a glance at the function's signature. The same goes for get_at
.
If we look at the way function append
is used, we can see that this function also belongs to the class sample_string
:
sample_string a , b;
...
a.append(b);
The syntax suggests us that we alter the state of object a
with some information from object b
, and that's exactly what happens. The call to function append
affects only the state of object a
and logically belongs with the data of that object.
Now, the function concat
. It is used like this:
sample_string a, b;
...
sample_string c = a.concat(b);
By looking at the syntax, we could come to the wrong conclusion that this function somehow alters the state of object a
. All that it does, though, is making a brand new object c
based on the data from both a
and c
. More, all the data needed for creation of object c
can be acquired through other public members, which means that this function does not really need direct access to class data.
Let's implement this operation as a non-member function.:
sample_string concat (const sample_string& left,
const sample_string& right);
This operation would be used like this:
sample_string a, b;
...
sample_string c = concat(a, b);
This time, the syntax suggests that we used some information from objects a
and b
to generate object c
, and that's exactly what we did. Code better reflects our intent, and therefore is more logical and readable.
Examples like this can be found in many real-life libraries and applications. For instance, take a look at .NET FCL System.String
class. It is immutable, but member functions such as Insert
, Trim
, Replace
really suggest that they alter the state of a string. If .NET supported non-member functions and constant arguments, function Insert
could be rewritten like this (MC++ syntax):
String __gc* Insert (const String __gc* source, int startIndex,
const String __gc* value);
While the name of the function is still somewhat misleading, the signature leaves no doubt what really happens.
Encapsulation
What about encapsulation? A lot has been written about how the usage of non-member functions improves encapsulation, but it comes down to this simple statement: non-member functions cannot access internal class implementation. If you can implement a functionality without messing with internal implementation, then it means better encapsulation.
Of course, this does not mean that we should try to implement each functionality as a non-member function in order to improve encapsulation. Sometimes a function needs direct access to internal class data, and in that case it may be necessary to implement it as a member. Even if a function needs read-only access to class data, in many cases it is a better idea to implement it as a member function. Introducing an accessor function to internal class data in order to implement a function as a non-member, is almost always a bad idea. For instance, in our example with class sample_string
it was OK to implement function concat
as a non-member because it can be implemented by using existing public functions get_length
and get_at
. However, if we didn't have those two functions, we would need to introduce a const
accessor in order to make concat
a non-member. If this accessor returns a copy of class data, then we are probably OK.
std::vector<char> sample_db::get_data() const
{
return data_;
}
Of course, it can be unacceptable to copy data like this in terms of performance, but if we talk about encapsulation, this is a good solution.
However, if the accessor returns a reference or a pointer to the data, then we actually hurt encapsulation.
const std::vector<char>& sample_db::get_data() const
{
return data_;
}
If we do that, we expose our class data directly to the class users, and that is bad. Imagine that at some point we decide to change internal class data to be std::string
rather than std::vector<char>
. In that case all the functions that use get_data
must be rewritten. Implementing concat
as a member function would hurt encapsulation much less than adding an accessor like this. In some cases, it might be a good solution to implement concat
as a friend non-member function. Of course, it wouldn't improve encapsulation, but it would still be more readable than a version with member function.
Coupling
Finally, let's see how the use of non-member functions affects object coupling. It is a good design practice to promote loose coupling between objects. The less objects know about each other, the better. On the other hand, objects need to interact in order to make system functional. To solve this problem, Mediator Design Pattern has been introduced. Mediators are classes that handle interaction between different types and thus promote loose coupling. However, there are cases when a non-member function is enough to serve the role of a mediator. For instance, assume we have a class sample_db
that wraps a database engine:
class sample_db {...};
Now, imagine that in an application of ours we need to write sample_string
objects to a sample_db
database. The first thing that may come to our mind would be to add a function to sample_db
that would take care of that:
class sample_db
{
...
public:
void write_sample_string (const sample_string& str);
};
That would work, but it requires sample_db
to know about sample_string
, and that is bad. If we want to reuse sample_db
in some other project, we would need to include sample_string
even if we don't need it. A better approach would be to make a non-member function in a separate compilation unit that would serve as a mediator between those two classes:
void write_sample_string_to_db (const sample_string& str, sample_db& db);
Of course, in order to make this work, sample_db
would need to have a public function that writes some form of data to a database.
Political incorrectness of non-member functions
So far, we have discussed only the technical side of the problem, and in ideal world that would be enough. Alas, we live in real world, and if you decide to use non-member functions in our programs, you may experience other kinds of problems. Namely, many people still think that non-member functions are a legacy from C that has no place in object oriented programming practices. This can raise several issues:
- During code reviews, your code may be attacked as "not object oriented". This can be particularly inconvenient if you are a junior developer, or relatively new in your organization. On the other hand, if you are already established and respected within your team, you can make use of code reviews to introduce the benefits of non-member functions to your co-workers.
- If your company ships the source to customers, some of them may also question the quality of your code from the same perspective. Again, in some cases you may be able to explain them the reasons for using non-member functions, but that depends on your relationship with the client, and the level of trust they have in you.
Conclusion
I hope this article will make some developers reconsider use of non-member functions in their OO programs. However, the last thing I want is to turn anybody from a "OO zealot" to a "non-member functions zealot". Be aware that there are cases when use of non-member functions makes sense, and cases when it does not. Use your knowledge, experience and instinct to decide when to implement operations as member functions and when to leave them out of classes. Always bear in mind that your ultimate goal is to produce high-quality code, and for that purpose use any programming technique that you believe will be appropriate for your specific tasks. Don't be a slave to any programming paradigm. Make them serve you.
References
- Bjarne Stroustrup: "The C++ Programming Language", Addison-Wesley Pub Co; ISBN: 0201889544 ; 3rd edition (June 20, 1997)
- Scott Meyers, How Non-Member Functions Improve Encapsulation
Born in Kragujevac, Serbia. Now lives in Boston area with his wife and daughters.
Wrote his first program at the age of 13 on a Sinclair Spectrum, became a professional software developer after he graduated.
Very passionate about programming and software development in general.