Name hiding
If you inherit a class and provide a new
definition for one of its member functions, there are two possibilities. The
first is that you provide the exact signature and return type in the derived
class definition as in the base class definition. This is called
redefining for ordinary member functions and
overriding when the base class member function is
a virtual function
(virtual functions are the normal case, and will be covered in detail in
Chapter 15). But what happens if you change the member function argument list or
return type in the derived class? Here’s an example:
//: C14:NameHiding.cpp
// Hiding overloaded names during inheritance
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//! d2.f(s); // string version hidden
Derived3 d3;
//! x = d3.f(); // return int version hidden
Derived4 d4;
//! x = d4.f(); // f() version hidden
x = d4.f(1);
} ///:~
In Base you see an overloaded
function f( ), and Derived1 doesn’t make any changes to
f( ) but it does redefine g( ). In main( ),
you can see that both overloaded versions of f( ) are available in
Derived1. However, Derived2 redefines one overloaded version of
f( ) but not the other, and the result is that the second overloaded
form is unavailable. In Derived3, changing the return type hides both the
base class versions, and Derived4 shows that changing the argument list
also hides both the base class versions. In general, we can say that anytime you
redefine an overloaded function name from the base class, all the other versions
are automatically hidden in the new class. In Chapter 15, you’ll see that
the addition of the virtual keyword affects function overloading a bit
more.
If you change the interface of the base
class by modifying the
signature
and/or
return
type of a member function from the base class, then you’re using the class
in a different way than inheritance is normally intended to support. It
doesn’t necessarily mean you’re doing it wrong, it’s just that
the ultimate goal of inheritance is to support
polymorphism, and if you change the function
signature or return type then you are actually changing the interface of the
base class. If this is what you have intended to do then you are using
inheritance primarily to reuse code, and not to maintain the common interface of
the base class (which is an essential aspect of polymorphism). In general, when
you use inheritance this way it means you’re taking a general-purpose
class and specializing it for a particular need – which is usually, but
not always, considered the realm of
composition.
For example, consider the Stack
class from Chapter 9. One of the problems with that class is that you had to
perform a cast every time you fetched a pointer from the container. This is not
only tedious, it’s unsafe – you could cast the pointer to anything
you want.
An approach that seems better at first
glance is to specialize the general Stack class using inheritance.
Here’s an example that uses the class from Chapter 9:
//: C14:InheritStack.cpp
// Specializing the Stack class
#include "../C09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class StringStack : public Stack {
public:
void push(string* str) {
Stack::push(str);
}
string* peek() const {
return (string*)Stack::peek();
}
string* pop() {
return (string*)Stack::pop();
}
~StringStack() {
string* top = pop();
while(top) {
delete top;
top = pop();
}
}
};
int main() {
ifstream in("InheritStack.cpp");
assure(in, "InheritStack.cpp");
string line;
StringStack textlines;
while(getline(in, line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) { // No cast!
cout << *s << endl;
delete s;
}
} ///:~
Since all of the member functions in
Stack4.h are inlines, nothing needs to be linked.
StringStack specializes
Stack so that push( ) will accept only String
pointers. Before, Stack would accept void pointers, so the user
had no type checking to make sure the proper pointers were inserted. In
addition, peek( ) and pop( ) now return String
pointers instead of void pointers, so no cast is necessary to use the
pointer.
Amazingly enough, this extra
type-checking safety is free in push( ), peek( ), and
pop( )! The compiler is being given extra type information that it
uses at compile-time, but the functions are inlined and no extra code is
generated.
Name hiding comes into play here because,
in particular, the push( ) function has a different signature: the
argument list is different. If you had two versions of push( ) in
the same class, that would be overloading, but in this case overloading is
not what we want because that would still allow you to pass any kind of
pointer into push( ) as a void*. Fortunately, C++ hides the
push(void*) version in the base class in favor of the new version
that’s defined in the derived class, and therefore it only allows us to
push( ) string pointers onto the StringStack.
Because we can now guarantee that we know
exactly what kind of objects are in the container, the destructor works
correctly and the ownership problem is solved – or
at least, one approach to the ownership problem. Here, if you
push( ) a string pointer onto the StringStack, then
(according to the semantics of the StringStack) you’re also
passing ownership of that pointer to the StringStack. If you
pop( ) the pointer, you not only get the pointer, but you also get
ownership of that pointer. Any pointers that are left on the StringStack
when its destructor is called are then deleted by that destructor. And since
these are always string pointers and the delete statement is
working on string pointers instead of void pointers, the proper
destruction happens and everything works correctly.
There is a drawback: this class works
only for string pointers. If you want a Stack that works
with some other kind of object, you must write a new version of the class so
that it works only with your new kind of object. This rapidly becomes tedious,
and is finally solved using templates, as you will see in Chapter
16.
We can make an additional observation
about this example: it changes the interface of the Stack in the process
of inheritance. If the interface is different, then a StringStack really
isn’t a Stack, and you will never be able to correctly use a
StringStack as a Stack. This makes the use of inheritance
questionable here; if you’re not creating a StringStack that
is-a type of Stack, then why are you
inheriting? A more appropriate version of StringStack will be shown later
in this
chapter.