Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Thinking in C++
Prev Contents / Index Next

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.

Thinking in C++
Prev Contents / Index Next

 
 
   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire