C++: Rationale Behind Hiding Rule

C++: rationale behind hiding rule

It's an hairy question, but apparently the idea is that this hiding feature helps avoiding subtle bugs when making changes to a base class (that could otherwise "steal" calls that before would have been handled by the derived class). Still a change in a base class can influence the result of compilation of derived classes so I don't think I understand 100% this explanation.

I agree that this topic is so frequently discussed that probably the hiding actually increases the amount of "surprises" in C++ programmers.

A detailed discussion about this issue can be found here...

What is the rationale behind the strict aliasing rule?

Since, in this example, all the code is visible to a compiler, a compiler can, hypothetically, determine what is requested and generate the desired assembly code. However, demonstration of one situation in which a strict aliasing rule is not theoretically needed does nothing to prove there are not other situations where it is needed.

Consider if the code instead contains:

foo(&val, ptr)

where the declaration of foo is void foo(uint64_t *a, uint32_t *b);. Then, inside foo, which may be in another translation unit, the compiler would have no way of knowing that a and b point to (parts of) the same object.

Then there are two choices: One, the language may permit aliasing, in which case the compiler, while translating foo, cannot make optimizations relying on the fact that *a and *b are different. For example, whenever something is written to *b, the compiler must generate assembly code to reload *a, since it may have changed. Optimizations such as keeping a copy of *a in registers while working with it would not be allowed.

The second choice, two, is to prohibit aliasing (specifically, not to define the behavior if a program does it). In this case, the compiler can make optimizations relying on the fact that *a and *b are different.

The C committee chose option two because it offers better performance while not unduly restricting programmers.

Reason for C++ member function hiding

Name lookup works by looking in the current scope for matching names, if nothing is found then it looks in the enclosing scope, if nothing is found it looks in the enclosing scope, etc. until reaching the global namespace.

This isn't specific to classes, you get exactly the same name hiding here:

#include <iostream>

namespace outer
{
void foo(char c) { std::cout << "outer\n"; }

namespace inner
{
void foo(int i) { std::cout << "inner\n"; }

void bar() { foo('c'); }
}
}

int main()
{
outer::inner::bar();
}

Although outer::foo(char) is a better match for the call foo('c') name lookup stops after finding outer::inner::foo(int) (i.e. outer::foo(char) is hidden) and so the program prints inner.

If member function name weren't hidden that would mean name lookup in class scope behaved differently to non-class scope, which would be inconsistent and confusing, and make C++ even harder to learn.

So there's no technical reason the name lookup rules couldn't be changed, but they'd have to be changed for member functions and other types of name lookup, it would make compilers slower because they'd have to continue searching for names even after finding matching names in the current scope. Sensibly, if there's a name in the current scope it's probably the one you wanted. A call in a scope A probably wants to find names in that scope, e.g. if two functions are in the same namespace they're probably related (part of the same module or library) and so if one uses the name of the other it probably means to call the one in the same scope. If that's not what you want then use explicit qualification or a using declaration to tell the compiler the other name should be visible in that scope.

Rationale behind using namespace behavior

A desirable property of a namespace system is that of what I call incremental API compatibility. That is, if I add a symbol to a namespace, then any previously working program should keep working and mean the same thing.

Now, plain C++ with overloads is not incrementally API compatible:

int foo(long x) { return 1; }

int main()
{
foo(0);
}

Now I add the overload int foo(int x) { return 2; } and the program silently changes meaning.

Anyway, when C++ people designed the namespace system they wanted that when incrementing an external API, previously working code should not change the namespace from where the symbol is chosen. From your example, the previous working code would be something like:

namespace A {
//no fn here, yet
}

namespace Inner {

int fn() { return 2; }

namespace B {

using namespace A;
int z = fn();
}
}

And z is easily initialized to 2. Now augmenting namespace A with a symbol named fn will not change the meaning of that working code.

The opposite case does not really apply:

namespace A {
int fn() { return 1; }
}

namespace Inner {

// no fn here

namespace B {

using namespace A;
int z = fn();
}
}

Here z is initialized to 1. Of course, if I add fn to Inner it will change the meaning of the program, but Inner is not an external API: actually, when Inner was written initially, A::fn did already exist (it was being called!), so there is no excuse for being unaware of the clash.



A somewhat practical example

Imagine this C++98 program:

#include <iostream>

namespace A {
int move = 0;
void foo()
{
using namespace std;
cout << move << endl;
return 0;
}
}

int main()
{
A::foo();
return 0;
}

Now, if I compile this with C++11, everything works fine thanks to this using rule. If using namespace std worked as applying using declarations for everything in that namespace, then this program would try to print function std::move instead of A::move.

Template parameter name hiding

This rule (specified in [temp.local]/9) is subject of an open core language issue created over 11 years ago - core issue #459. The CWG discussed this thoroughly. About the intent, Mike Miller mentions that

The rationale for the current specification is really very simple:

  • “Unless redeclared in the derived class, members of a base class are also considered to be members of the derived class.” (10
    [class.derived] paragraph 2)

  • In class scope, members hide nonmembers.


That's it. Because template parameters are not members, they are
hidden by member names (whether inherited or not). I don't find that
“bizarre,” or even particularly surprising.

Rationale:

We have some sympathy for a change, but the current rules fall straightforwardly out of the lookup rules, so they're not “wrong.” Making private members invisible also would solve this problem. We'd be willing to look at a paper proposing that.[..]
The CWG decided not to consider a change to the existing rules at this time without a paper exploring the issue in more detail.

Unfortunately no such paper has been written yet, and so the rule persists until today.

Hiding of all overloaded methods in base class

The fundamental reason is to make code more robust.

struct Base {
};

struct Derived : Base {
void f(long);
void g() { f(3); } // calls Derived::f
}

Now suppose Base is defined in a library, and you get an update to that library and the update changes the definition of Base:

struct Base {
void f(int);
};

Now suppose that searches for overloaded functions didn't stop when a name was found. In that case, Derived::g would call Base::f instead of Derived::f, and your derived class would quietly do something completely different from what it did before, and different from what it was designed and documented to do.



Related Topics



Leave a reply



Submit