I'll start with the punchline: If you're writing a function that can be implemented as either a member or as a non-friend non-member, you should prefer to implement it as a non-member function. That decision increases class encapsulation. When you think encapsulation, you should think non-member functions.
Surprised? Read on.
When I wrote the first edition of Effective C++ in 1991 [1], I examined the problem of determining where to declare a function that was related to a class. Given a class C and a function f related to C, I developed the following algorithm:
This algorithm served me well through the years, and when I revised Effective C++ for its second edition in 1997 [2], I made no changes to this part of the book.if (f needs to be virtual )
make f a member function of C;
else if (f is operator >> or
operator << )
{
make f a non-member function;
if (f needs access to non-public
members of C)
make f a friend of C;
}
else if (f needs type conversions
on its left - most argument)
{
make f a non-member function;
if (f needs access to
non-public members of C)
make f a friend of C;
}
else
make f a member function of C;
Since then, I've been battling programmers who've taken to heart the lesson that being object-oriented means putting functions inside the classes containing the data on which the functions operate. After all, they tell me, that's what encapsulation is all about. They are mistaken.if (f needs to be virtual )
make f a member function of C;
else if (f is operator >> or
operator << )
{
make f a non-member function;
if (f needs access to non-public
members of C)
make f a friend of C;
}
else if (f needs type conversions
on its left - most argument)
{
make f a non-member function;
if (f needs access to non-public
members of C)
make f a friend of C;
}
else if (f can be implemented via C ' s
public interface )
make f a non - member function;
else
make f a member function of C;
The weakness of this struct is that it's not flexible in the face of change. Once clients started using this struct, it would, practically speaking, be very hard to change it; too much client code would be broken. If we later decided we wanted to compute x and y instead of storing those values, we'd probably be out of luck. We'd be similarly thwarted if we decided a superior design would be to look x y up in a database. This is the real problem with poor encapsulation: it precludes future implementation changes. Unencapsulated software is inflexible, and as a result, it's not very robust. When the world changes, the software is unable to gracefully change with it. (Remember that we're talking here about what is practical, not what is possible. It's clearly possible to change struct Point, Now consider a class with an interface that offers clients capabilities similar to those afforded by the struct above, but with an encapsulated implementation: and but if enough code is dependent on it in its current form, it's not practical.)struct Point {
int x, y;
} ;
This interface supports the implementation used by the struct (storing x and y as ints), but it also affords alternative implementations, such as those based on computation or database lookup. This is a more flexible design, and the flexibility makes the resulting software more robust. If the class's implementation is found lacking, it can be changed without requiring changes to client code. Assuming the declarations of the public member functions remain unchanged, client source code is unaffected. ( If a suitable implementation has been adopted [4] , clients need not even recompile.) Encapsulated software is more flexible than unencapsulated software, and, all other things being equal, that flexibility makes it the superior design choice.class Point {
public:
int getXValue() const;
int getYValue() const;
void setXValue(int newXValue);
void setYValue(int newYValue);
private:
... // whatever...
} ;
// a design less encapsulated than it could beA better design is to move make out of Widget, thus increasing the overall encapsulation of the system. To show that Widget and make are related, the proper tool is a namespace:
class Widget {
... // all the Widget stuff; may be
// public, private, or protected
public:
// could also be a non-friend non-member
static Widget* make(/* params */);
};
// a more encapsulated designAlas, there is a weakness to this design when templates enter the picture. For details, see the accompanying sidebar.
namespace WidgetStuff {
class Widget { ... };
Widget* make( /* params */ );
};
class Wombat {That would lead to a syntactic inconsistency for class clients, because for a Wombat w, they'd write
public:
void eat(double tonsToEat);
...
};
void sleep(Wombat& w, double hoursToSnooze);
w.eat(.564);to make it eat, but they would write
sleep(w, 2.57);to make it sleep. Using only member functions, things would look much neater:
class Wombat {Ah, the uniformity of it all! But this uniformity is misleading, because there are more functions in the world than are dreamt of by your philosophy. To put it bluntly, non-member functions happen. Let us continue with the Wombat example. Suppose you write software to model these fetching creatures, and imagine that one of the things you frequently need your Wombats to do is sleep for precisely half an hour. Clearly, you could litter your code with calls to w.sleep(.5), but that would be a lot of .5s to type, and at any rate, what if that magic value were to change? There are a number of ways to deal with this issue, but perhaps the simplest is to define a function that encapsulates the details of what you want to do. Assuming you're not the author of Wombat, the function will necessarily have to be a non-member, and you'll have to call it as such:
public:
void eat(double tonsToEat);
void sleep(double hoursToSnooze);
...
};
w.eat(.564);
w.sleep(2.57);
// might be inline, but it doesn't matter
void nap(Wombat& w) { w.sleep(.5); }
Wombat w;
...
nap(w);
And there you have it, your dreaded syntactic inconsistency. When you want to feed your wombats, you make member function calls, but when you want them to nap, you make non-member calls.
If you reflect a bit and are honest with yourself, you'll admit that you have this alleged inconsistency with all the nontrivial classes you use, because no class has every function desired by every client. Every client adds at least a few convenience functions of their own, and these functions are always non-members. C++ programers are used to this, and they think nothing of it. Some calls use member syntax, and some use non-member syntax. People just look up which syntax is appropriate for the functions they want to call, then they call them. Life goes on. It goes on especially in the STL portion of the Standard C++ library, where some algorithms are member functions (e.g., size), some are non-member functions (e.g., unique), and some are both (e.g., find). Nobody blinks. Not even you.
Herb Sutter has explained that the "interface" to a class (roughly speaking, the functionality provided by the class) includes the non-member functions related to the class, and he's shown that the name lookup rules of C++ support this meaning of "interface" [7,8]. This is wonderful news for my "non-friend non-members are better than members" argument, because it means that the decision to make a class-related function a non-friend non-member instead of a member need not even change the interface to that class! Moreover, the liberation of the functions in a class's interface from the confines of the class definition leads to some wonderful packaging flexibility that would otherwise be unavailable. In particular, it means that the interface to a class may be split across multiple header files.
Suppose the author of the Wombat class discovered that Wombat clients often need a number of convenience functions related to eating, sleeping, and breeding. Such convenience functions are by definition not strictly necessary. The same functionality could be obtained via other (albeit more cumbersome) member function calls. As a result, and in accord with my advice in this article, each convenience function should be a non-friend non-member. But suppose the clients of the convenience functions for eating rarely needed the convenience functions for sleeping or breeding. And suppose the clients of the sleeping and breeding convenience functions also rarely needed the convenience functions for eating and, respectively, breeding and sleeping.
Rather than putting all Wombat-related functions into a single header file, a preferable design would be to partition the Wombat interface across four separate headers, one for core Wombat functionality (primarily the class definition), and one each for convenience functions related to eating, sleeping, and breeding. Clients then include only the headers they need. The resulting software is not only clearer, it also contains fewer gratuitous compilation dependencies [4,9]. This multiple-header approach was adopted for the standard library. The contents of namespace std are spread across 50 different headers. Clients #include the headers declaring the parts of the library they care about, and they ignore everything else.
In addition, this approach is extensible. When the declarations for the functions making up a class's interface are spread across multiple header files, it becomes natural for clients creating application-specific sets of convenience functions to cluster those functions into a new header file and to #include that file as appropriate. In other words, to treat the application-specific convenience functions just like they treat the convenience functions provided by the author of the class. This is as it should be. After all, they're all just convenience functions.
In Effective C++, I argued for class interfaces that are complete and minimal [10]. Such interfaces allow class clients to do anything they might reasonably want to do, but classes contain no more member functions than are absolutely necessary. Adding functions beyond the minimum necessary to let clients get their jobs done, I wrote, decreases the class's comprehensibility and maintainability, plus it increases compilation times for clients. Jack Reeves has written that the addition of member functions beyond those truly required violates the open/closed principle, yields fat class interfaces, and ultimately leads to software rot [11]. That's a fair number of arguments for minimizing the number of member functions in a class, but now we have an additional reason: failure to do so decreases a class's encapsulation.
Of course, a minimal class interface is not necessarily the best interface. I remarked in Effective C++[10]. Based on his work with various string-like classes, Jack Reeves has observed that some functions just don't "feel" right when made non-members, even if they could be non-friend non-members [12]. The "best" interface for a class can be found only by balancing many competing concerns, of which the degree of encapsulation is but one. that adding functions beyond those truly necessary may be justifiable if it significantly improves the performance of the class, makes the class easier to use, or prevents likely client errors
Still, the lesson of this article should be clear. Conventional wisdom notwithstanding, use of non-friend non-member functions improves a class's encapsulation, and a preference for such functions over member functions makes it easier to design and develop classes with interfaces that are complete and minimal (or close to minimal). Arguments about the naturalness of the resulting calling syntax are generally unfounded, and adoption of a predilection for non-friend non-member functions leads to packaging strategies for a class's interface that minimize client compilation dependencies while maximizing the number of convenience functions available to them.
It's time to abandon the traditional, but inaccurate, ideas of what it means to be object-oriented. Are you a true encapsulation believer? If so, I know you'll embrace non-friend non-member functions with the fervor they deserve.
Thanks to Arun Kundu for asking the question that led to this article. Thanks also to Jack Reeves, Herb Sutter, Dave Smallberg, Andrei Alexandrescu, Bruce Eckel, Bjarne Stroustrup, and Andrew Koenig for comments on pre-publication drafts that weren't as good as they should have been. (That's why they were drafts.) Finally, great thanks to Adela Novak for organizing the C++ seminars in Lucerne (Switzerland) that led to the many hours on planes and trains that allowed me to write the initial draft of this article.
[1] Scott Meyers. Effective C++: 50 Specific Ways to Improve Your Programs and Designs, First Edition (Addison-Wesley, 1991), Item 19.
[2] Scott Meyers. Effective C++, Second Edition (Addison-Wesley, 1998).
[3] The algorithm remains unchanged in current printings of Effective C++, because I'd have to also add the requisite reasoning (this article), and making such a substantial change to a book already in production simply isn't practical.
[4] Effective C++, Item 34.
[5] Erich Gamma et al. Design Patterns, Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995). Also known as the GOF or "Gang of Four" book.
[6] James O. Coplien. Advanced C++: Programming Styles and Idioms (Addison-Wesley, 1991).
[7] Herb Sutter. "Sutter's Mill: What's in a Class?" C++ Report, March 1998.
[8] Herb Sutter. Exceptional C++ (Addison-Wesley, 1999), Items 31-34.
[9] John Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996).
[10] Effective C++, Item 18.
[11] Jack Reeves. "(B)leading Edge: How About Namespaces?," C++ Report, April 1999.
[12] Jack Reeves. Personal communication.