We conclude this miniseries by considering some implications of the interface principle on Name Lookup. Can you spot the (quite subtle) problem lurking in the following code?
What is name hiding? Show how it can affect the visibility of base class names in Derived classes.
Will the following example compile correctly? Make your answer as complete as you can. Try to isolate and explain any areas of doubt.
// Example 2: Will this compile? //// In some library header:namespace N { class C {}; }int operator+(int i, N::C) { return i+1; }// A mainline to exercise it:#include <numeric>int main(){ N::C a[10]; std::accumulate(a, a+10, 0);}
Solution
Let's recap a familiar inheritance issue: Name hiding, by answering question 1 in the item:
What is name hiding? Show how it can affect the visibility of base class names in Derived classes.
Name hiding
Consider the following example:
// Example 1a: Hiding a name // from a base class//struct B{ int f( int ); int f( double ); int g( int );};struct D : public B{private: int g( std::string, bool );};D d;int i;d.f(i); // ok, means B::f(int)d.g(i); // error: g takes 2 args
Most of us shoshould be used to seeing this kind of name hiding, although the fact that the last line won't compile surprises most new C ++ programmers. in short, when we declare a function namedGIn the derived classD, It automatically hides all functions with the same name in all direct and indirect base classes. It doesn't matter a whit thatD: G"Obviusly" can't be the function that the programmer meant to call (not only doesD: GHave the wrong signature, but it's private and, therefore, inaccessible to boot), becauseB: GIs hidden and can't be considered by Name Lookup.
To see what's really going on, let's look in a little more detail at what the compiler does when it encounters the function callD. G (I). First, it looks in the immediate scope, in this case the scope of classD, And makes a list of all functions it can find that are namedG(Regardless of whether they're accessible or even take the right number of parameters ). only if it doesn' t find any at all does it then continue "outward" into the next enclosing scope and repeat limit n this case, the scope of the base classBDefine ntil it eventually either runs out of scopes without having found a function with the right name or else finds a scope that contains at least one candidate function. if a scope is found that has one or more candidate functions, the compiler then stops searching and works with the candidates that it's found, faster overload resolution and then applying access rules.
There are very good reasons why the language must work this way.[9] to take the extreme case, it makes intuitive sense that a member function that's a near-exact match ought to be preferred over a global function that wocould have been a perfect match had we considered the parameter types only.
[9] For example, one might think that if none of the functions found in an inner scope were usable, then it cocould be okay to let the compiler start searching further enclosing scopes. that wocould, however, produce surprising results in some cases (consider the case in which there's a function that wocould be an exact match in an outer scope, but there's a function in an inner scope that's a close match, requiring only a few parameter conversions ). or, one might think that the compiler shoshould just make a list of all functions with the required name in all scopes and then perform overload resolution into SS scopes. but, alas, that too has its pitfalls (consider that a member function ought to be preferred over a global function, rather than result in a possible ambiguity ).
How to work around und unwanted name hiding
Of course, there are the two usual ways around the name-hiding problem in example 1A. first, the calling code can simply say which one it wants and force the compiler to look in the right scope.
// Example 1b: Asking for a name // from a base class//D d;int i;d.f(i); // ok, means B::f(int)d.B::g(i); // ok, asks for B::g(int)
Second, and usually more appropriate, the designer of classDCan makeB: GVisible with a using declaration. This allows the compiler to considerB: GIn the same scopeD: GFor the purposes of Name Lookup and subsequent overload resolution.
// Example 1c: Un-hiding a name // from a base class//struct D : public B{ using B::g;private: int g( std::string, bool );};
Either of these gets around the hiding problem in the original example 1A code.
Namespaces and the interface Principle
Use namespaces wisely. if you put a class into a namespace, be sure to put all helper functions and operators into the same namespace too. if you don't, you may discover surprising effects in your code.
The following simple program is based on code e-mailed to me by astute reader Darin Adler. It supplies a classCIn namespaceNAnd an operation on that class. notice thatOperator + ()Is in the global namespace, not in namespaceN. Does that matter? Isn' t the code valid as written anyway?
Question 2, you will remember, was:
Will the following example compile correctly? Make your answer as complete as you can. Try to isolate and explain any areas of doubt.
// Example 2: Will this compile? //// In some library header:namespace N { class C {}; }int operator+(int i, N::C) { return i+1; }// A mainline to exercise it:#include <numeric>int main(){ N::C a[10]; std::accumulate(a, a+10, 0);}
Before reading on, stop and consider the hints I 've dropped so far: Will this program compile?[10] Is it portable?
[10] in case you're re wondering that there might be a potential portability problem depending on whether the implementationSTD: Accumulate ()InvokesOperator + (INT, N: C)OrOperator + (n: C, INT), There isn't. The standard says that it must be the former, so Example 1 is providingOperator + ()With the correct signature.
Name hiding in nested namespaces
Well, at first glance, Example 2 sure looks legal. so the answer is probably surprising: Maybe it will compile, maybe not. it depends entirely on your implementation, and I know of standard-Conforming implementations that will compile this program correctly and equally standard-Conforming implementations that won't. gather 'round, and I'll show you why.
The key to understanding the answer is understanding what the compiler has to do insideSTD: Accumulate.STD: AccumulateTemplate looks something like this:
namespace std { template<class Iter, class T> inline T accumulate( Iter first, Iter last, T value ) { while( first != last ) { value = value + *first; // 1 ++first; } return value; }}
The code in example 2 actually cballsSTD: Accumulate <n: C *, int>. In line 1 above, how shoshould the compiler interpret the expressionValue + * first? Well, it's got to look forOperator + ()That takesIntAndN: c(Or parameters that can be convertedIntAndN: c). Hey, it just so happens that we have just suchOperator + (INT, N: C)At global scope! Look, there it is! Cool. So everything must be fine, right?
The problem is that the compiler may or may not be able to seeOperator + (INT, N: C)At global scope, depending on what other functions have already been seen to be declared in namespaceSTDAt the point whereSTD: Accumulate <n: C *, int>Is instantiated.
To see why, consider that the same name hiding we observed with derived classes happens with any nested scopes, including namespaces, and consider where the compiler starts looking for a suitableOperator + (). (Now I'm going to reuse my explanation from the earlier section, only with a few names substituted.) First, it looks in the immediate scope, in this case the scope of namespaceSTD, And makes a list of all functions it can find that are namedOperator + ()(Regardless of whether they're accessible or even take the right number of parameters ). only if it doesn' t find any at all does it then continue "outward" into the next enclosing scope and repeat limit n this case, the scope of the next enclosing namespace outsideSTD, Which happens to be the global scope into ntil it eventually either runs out of scopes, without having found a function with the right name, or else finds a scope that contains at least one candidate function. if a scope is found that has one or more candidate functions, the compiler then stops searching and works with the candidates it's found, specify Ming overload resolution and applying access rules.
In short, whether Example 2 will compile depends entirely on whether this implementation's version of the Standard HeaderNumeric: A) declaresOperator + ()(AnyOperator + (), Suitable or not, accessible or not); or B) includes any other standard header that does so. unlike standard C, standard C ++ does not specify which standard headers will include each other, so when you includeNumeric, You may or may not get HeaderIteratorToo, for example, which does define severalOperator + ()Functions. I know of C ++ products that won't compile example 2, others that will compile Example 2 but balk once you add the line# Include <vector>, And so on.
Some fun with Compilers
It's bad enough that the compiler can't find the right function if there happens to be anotherOperator + ()In the way, but typicallyOperator + ()That does get encountered in a standard header is a template, and compilers generate notoriously difficult-to-read error messages when templates are involved. for example, one popular implementation reports the following errors when compiling Example 2 (note that in this implementation, the headerNumericDoes in fact include the headerIterator).
error C2784: 'class std::reverse_iterator<'template-parameter- 1','template-parameter-2','template-parameter-3','template-parameter-4','template-parameter-5'> __cdecl std::operator+(template-parameter-5,const classstd::reverse_iterator<'template-parameter-1','template-parameter-2','template-parameter-3','template-parameter-4','template-parameter-5'>&)' : could not deduce template argument for'template-parameter-5' from 'int'error C2677: binary '+' : no global operator defined which takestype 'class N::C' (or there is no acceptable conversion)
Yikes! Imagine the poor programmer's confusion.
The first error message is unreadable. the compiler is merely complaining (as clearly as it can) that it did findOperator + ()But can't figure out how to use it in an appropriate way. But that doesn't help the poor programmer. "Huh? "Saith the programmer, scratching at his scalkaline beneath his forelock," When did I ever ask forReverse_iteratorAnywhere? "
The second message is a flagrant lie, and it's the compiler vendor's fault (although perhaps an understandable mistake, because the message was probably right in most of the cases in which it came up before people began to use namespaces widely ). it's close to the correct message "No operator found which takes ..., "But that doesn' t help the poor programmer either." Huh? "Saith the programmer, now indignant with ire," There is too a global operator defined that takes type'Class N: C'! "
How is a mortal programmer ever to decipher what's going wrong here? And, once he does, how loudly is he likely to curse the author of classN: c? Best to avoid the problem completely, as we shall now see.
The solution
When we encountered this problem in the familiar guise of base/derived name hiding, we had two possible solutions: have the calling Code explicitly say which function it wants (example 1B), or writeUsingDeclaration to make the desired function visible in the Right scope (example 1c). Neither solution works in this case. The first is possible[11] But places an unacceptable burden on the programmer; the second is impossible.
[11] by requiring the programmer to use the versionSTD: AccumulateThat takes a predicate and explicitly say which one he wants each time... A good way to lose MERs.
The real solution is to put ourOperator + ()Where it has always truly belonged and where it shoshould have been put in the first place: In namespaceN.
// Example 2b: Solution //// in some library headernamespace N{ class C {}; int operator+(int i, N::C) { return i+1; }}// a mainline to exercise it#include <numeric>int main(){ N::C a[10]; std::accumulate(a, a+10, 0); // now ok}
This code is portable and will compile on all conforming compilers, regardless of what happens to be already defined inSTDOr any other namespace. Now thatOperator + ()Is in the same namespace as the second parameter, when the compiler tries to resolve"+"Call insideSTD: Accumulate, It is able to see the rightOperator + ()Because of Koenig lookup. recall that Koenig lookup says that, in addition to looking in all the usual scopes, the compiler shall also look in the scopes of the function's parameter types to see if it can find a match.N: cIs In namespaceN, So the compiler looks in namespaceN, And happily finds exactly what it needs, no matter how many otherOperator + ()'S happen to be lying around and cluttering up namespaceSTD.
The conclusion is that the problem arose because Example 2 did not follow the interface principle:
For a class X, all functions, including free functions, that both
"Mention" x
Are "supplied with" x
Are logically part of X, because they form part of the interface of X.
If an operation, even a free function (and especially an operator) mentions a class and is intended to form part of the interface of a class, then always be sure to supply it with the class named hich means, among other things, to put it in the same namespace as the class. the problem in example 2 arose because we wrote a classCAnd put part of its interface in a different namespace. making sure that the class and the interface stay together is the right thing to do in any case, and is a simple way of avoiding complex Name Lookup problems later on, when other people try to use your class.
Use namespaces wisely. either put all of a class inside the same namespace into ncluding things that to innocent eyes don't look like they're part of the class, such as free functions that mention the class (don't forget the interface principle) Doesn R don't put the class in a namespace at all. your users will thank you.
Guideline
|
Use namespaces wisely. if you write a class in some namespaceN, Be sure to put all helper functions and operatorsN, Too. If you don't, you may discover surprising effects in your code. |