I l@ve RuBoard |
![]() ![]() |
Solution![]() We'll start off with the deceptively simple question: What functions are "part of" a class, or make up the interface of a class? The deeper questions are:
So what's in a class? Here's the definition of "class" again:
Programmers often unconsciously misinterpret this definition, saying instead: "Oh yeah, a class, that's what appears in the class definition梩he member data and the member functions." But that's not the same thing, because it limits the word functions to mean just member functions. Consider: //*** Example 1 (a) class X { /*...*/ }; /*...*/ void f( const X& ); The question is: Is f part of X? Some people will automatically say "No" because f is a nonmember function (or free function). Others might realize something fundamentally important: If the Example 1 (a) code appears together in one header file, it is not significantly different from: //*** Example 1 (b) class X { /*...*/ public: void f() const; }; Think about this for a moment. Besides access rights,[2] f is still the same, taking a pointer/reference to X. The this parameter is just implicit in the second version, that's all. So, if Example 1 (a) all appears in the same header, we're already starting to see that even though f is not a member of X, it's nonetheless strongly related to X. I'll show exactly what that relationship is in the next section.
On the other hand, if X and f do not appear together in the same header file, then f is just some old client function, not a part of X (even if f is intended to augment X). We routinely write functions with parameters whose types come from library headers, and clearly our custom functions aren't part of those library classes. With that example in mind, I propose the Interface Principle:
By definition, every member function is "part of" X:
Applying the Interface Principle to Example 1 (a) gives the same result as our original analysis. Clearly, f mentions X. If f is also "supplied with" X (for example, if they come in the same header file and/or namespace[3]), then according to the Interface Principle, f is logically part of X because it forms part of the interface of X.
So the Interface Principle is a useful touchstone to determine what is really "part of" a class. Do you find it unintuitive that a free function should be considered part of a class? Then let's give real weight to this example by giving a more common name to f. //*** Example 1 (c) class X { /*...*/ }; /*...*/ ostream& operator<<( ostream&, const X& ); Here the Interface Principle's rationale is perfectly clear, because we understand how this particular free function works. If operator<< is "supplied with" X (for example, in the same header and/or namespace), then operator<< is logically part of X because it forms part of the interface of X. That makes sense even though the function is a nonmember, because we know that it's common practice for a class's author to provide operator<<. If, instead, operator<< comes, not from X's author, but from client code, then it's not part of X because it's not "supplied with" X.[4]
In this light, then, let's return to the traditional definition of a class:
That definition is exactly right, for it doesn't say a thing about whether the "functions" in question are members or not. I've been using C++ terms like "namespace" to describe what "supplied with" means, so is the Interface Principle C++-specific? Or is it a general OO principle that can apply in other languages? Consider a familiar example from another (in fact, a non-OO) language. /*** Example 2 (a) ***/ struct _iobuf { /*...data goes here...*/ }; typedef struct _iobuf FILE; FILE* fopen ( const char* filename, const char* mode ); int fclose( FILE* stream ); int fseek ( FILE* stream, long offset, int origin ); long ftell ( FILE* stream ); /* etc. */ This is the standard "handle technique" for writing OO code in a language that doesn't have classes. You provide a structure that holds the object's data and functions梟ecessarily nonmembers梩hat take or return pointers to that structure. These free functions construct (fopen), destroy (fclose), and manipulate (fseek, ftell, and so forth) the data. This technique has disadvantages (for example, it relies on client programmers to refrain from fiddling with the data directly), but it's still "real" OO code梐fter all, a class is "a set of data, along with the functions that operate on that data." In this case, of necessity, the functions are all nonmembers, but they are still part of the interface of FILE. Now consider an "obvious" way to rewrite Example 2 (a) in a language that does have classes. //*** Example 2 (b) class FILE { public: FILE( const char* filename, const char* mode ); ~FILE(); int fseek( long offset, int origin ); long ftell(); /* etc. */ private: /*...data goes here...*/ }; The FILE* parameters have just become implicit this parameters. Here it's clear that fseek is part of FILE, just as it was in Example 2 (a), even though there it was a nonmember. We can even merrily make some functions members and some not. //*** Example 2 (c) class FILE { public: FILE( const char* filename, const char* mode ); ~FILE(); long ftell(); /* etc. */ private: /*...data goes here...*/ }; int fseek( FILE* stream, long offset, int origin ); It really doesn't matter whether the functions are members. As long as they "mention" FILE and are "supplied with" FILE, they really are part of FILE. In Example 2 (a), all the functions were nonmembers, because in C they have to be. Even in C++, some functions in a class's interface have to be (or should be) nonmembers. operator<< can't be a member, because it requires a stream as the left-hand argument, and operator+ shouldn't be a member in order to allow conversions on the left-hand argument. A Deeper Look at Koenig LookupThe Interface Principle makes even more sense when you realize that it does exactly the same thing as Koenig lookup. I'll use two examples to illustrate and define Koenig lookup. In the next section, I'll use the Myers Example to show why this is directly related to the Interface Principle. Here's why we need Koenig lookup, using the same example as in Item 31. It's right out of the C++ standard. //*** Example 3 (a) namespace NS { class T { }; void f(T); } NS::T parm; int main() { f(parm); // OK: calls NS::f } Pretty nifty, isn't it? "Obviously" the programmer shouldn't have to explicitly write NS::f(parm), because just f(parm) "obviously" means NS::f(parm), right? But what's obvious to us isn't always obvious to a compiler, especially considering there's nary a "using" in sight to bring the name f into scope. Koenig lookup lets the compiler do the right thing. Here's how it works: Recall that "name lookup" just means that, whenever you write a call like "f(parm)", the compiler has to figure out which function named f you want. (With overloading and scoping, there could be several functions named f.) Koenig lookup says that, if you supply a function argument of class type (here parm, of type NS::T), then to find the function name the compiler is required to look, not just in the usual places, such as the local scope, but also in the namespace (here, NS) that contains the argument's type.[5] So Example 3 (a) works: The parameter being passed to f is a T, T is defined in namespace NS, and the compiler can consider the f in namespace NS梟o fuss, no muss.
It's good that we don't have to explicitly qualify f, because sometimes we can't easily qualify a function name. //*** Example 3 (b) #include <iostream> #include <string> // this header declares the function // std::operator<< for strings int main() { std::string hello = "Hello, world"; std::cout << hello; // OK: calls std::operator<< } Here the compiler has no way to find operator<< without Koenig lookup, because the operator<< we want is a free function that's made known to us only as part of the string package. It would be disgraceful if the programmer were forced to qualify this function name, because then the last line couldn't use the operator naturally. Instead, we would have to write either "std::operator<<( std::cout, hello );" which is exceedingly ugly, or "using std::operator<<;" which is annoying and quickly becomes tedious if there are many operators, or "using namespace std;" which dumps all the names in std into the current namespace and thus eliminates much of the advantage of having namespaces in the first place. If those options send shivers down your spine, you understand why we need Koenig lookup. In summary, if in the same namespace you supply a class and a free function that mentions that class,[6] the compiler will enforce a strong relationship between the two.[7] And that brings us back to the Interface Principle, because of the Myers Example.
More Koenig Lookup: The Myers ExampleConsider first a (slightly) simplified example. //*** Example 4 (a) namespace NS // this part is { // typically from class T { }; // some header } // file T.h void f( NS::T ); int main() { NS::T parm; f(parm); // OK: calls global f } Namespace NS supplies a type T, and the outside code provides a global function f that happens to take a T. This is fine, the sky is blue, the world is at peace, and everything is wonderful. Time passes. One fine day, the author of NS helpfully adds a function: //*** Example 4 (b) namespace NS // typically from { // some header T.h class T { }; void f( T ); // <-- new function } void f( NS::T ); int main() { NS::T parm; f(parm); // ambiguous: NS::f } // or global f? Adding a function in a namespace scope "broke" code outside the namespace, even though the client code didn't write using to bring NS's names into its scope! But wait, it gets better桸athan Myers pointed out the following interesting behavior with namespaces and Koenig lookup. //*** The Myers Example: "Before" namespace A { class X { }; } namespace B { void f( A::X ); void g( A::X parm ) { f(parm); // OK: calls B::f } } This is fine, the sky is blue. One fine day, the author of A helpfully adds another function. //*** The Myers Example: "After" namespace A { class X { }; void f( X ); // <-- new function } namespace B { void f( A::X ); void g( A::X parm ) { f(parm); // ambiguous: A::f or B::f? } } "Huh?" you might ask. "The whole point of namespaces is to prevent name collisions, isn't it? But adding a function in one namespace actually seems to 'break' code in a completely separate namespace." True, namespace B's code seems to break merely because it mentions a type from A. B's code didn't write a using namespace A; anywhere. It didn't even write using A::X;. This is not a problem, and B is not broken. This is in fact exactly what should happen.[8] If there's a function f(X) in the same namespace as X, then, according to the Interface Principle, f is part of the interface of X. It doesn't matter a whit that f happens to be a free function; to see clearly that it's nonetheless logically part of X, just give it another name.
//*** Restating the Myers Example: "After" namespace A { class X { }; ostream& operator<<( ostream&, const X& ); } namespace B { ostream& operator<<( ostream&, const A::X& ); void g( A::X parm ) { cout << parm; // ambiguous: A::operator<< or } // B::operator<<? }
If client code supplies a function that mentions X and matches the signature of one provided in the same namespace as X, the call should be ambiguous. B should be forced to say which competing function it means, its own or that supplied with X. This is exactly what we should expect given the Interface Principle:
What the Myers Example means is simply that namespaces aren't quite as independent as people originally thought, but they are still pretty independent and they fit their intended uses. In short, it's no accident that the Interface Principle works exactly the same way as Koenig lookup. Koenig lookup works the way it does fundamentally because of the Interface Principle. "How Strong Is the 'Part of' Relationship?" shows why a member function is still more strongly related to a class than a nonmember. ![]() |
I l@ve RuBoard |
![]() ![]() |