Public inheritance always creates an “is-a” relationship

The object-oriented paradigm defines inheritance as a relationship between two classes, were the derived class is a kind of the base class. This logical concept is independent of the programming language. Therefore, object-oriented languages will offer different implementations of this concept. For example, some languages will allow multiple inheritance while other languages support single inheritance only. Furthermore, some languages will add additional kinds of inheritance. These kinds of inheritance add some implementation techniques. So, they extend the inheritance system and allow a logical inheritance, which follows the object-oriented paradigm, but also allows a technical inheritance. Both implementation techniques can be called “inheritance” but they are very different from a software design perspective.

Within this article I want to have a look on the C++ inheritance system. C++ supports logical and technical inheritance. If you define a derived class and set the base class, you can select between “public”, “protected” and “private” inheritance. These keywords do not only change the visibility of methods, they represent completely different programming paradigms. That’s an important difference which is sometimes underrated and misunderstood. Therefore, I want to introduce this topics within this and the next article. We will start with public inheritance, which allows a logical relationship, and within the next article we will continue with private and protected inheritance which is used to create a technical relationship between objects.

 

X “is-a” Y

Public inheritance in C++ creates a relationship between a base and a derived object. This relationship is, according to the object-oriented paradigm, a “is-a” relationship. You should keep this simple rule in mind and should always check whether the “is-a” rule is fulfilled. Otherwise inheritance may not be a good decision. For example, a SUV is a car and a SUV has an engine. The “is-a” relationship between SUV and car can be implemented by public inheritance. But in contrast, the SUV is not an engine. Here we have a “has-an” relationship. This can be implemented by using composition.

This simple rule sounds very easy, and it is, but unfortunately inheritance is sometimes overused and especially novice developers will often use inheritance instead of composition. This creates wrong and unnecessary strong dependencies between objects and makes the code harder to maintain.

 

Derived is-a Base, but not vice versa

As the derived class is-a base class too, it can be used whenever the base class is needed. The following example shows an according function call which expects a base class but we pass a derived object instance, which is totally fine.

class Animal {};
class Bird : public Animal {};

void DoSomething1(Animal x)
{
}

void DoSomething2(Bird x)
{
}

int _tmain(int argc, _TCHAR* argv[])
{
  Animal animal;
  Bird bird;

  DoSomething1(animal);   // OK
  DoSomething1(bird);     // OK, a bird is-a animal

  DoSomething2(animal);   // Error, a animal is not a bird
  DoSomething2(bird);     // OK

  return 0;
}

 

As you can see, the function calls will work like expected and the compiler detects whether a wrong type is used.

But there is an important question to answer: if we expect a base class and pass a derived class instead, will we then use the base class functionality or the derived class functionality?

Let’s answer this question with a little test. We add a function to the base class and overwrite it in the derived class.

class Animal 
{
public:
  virtual void WhoAreYou()
  {
    std::cout << "I am an Animal." << std::endl;
  }
};

class Bird : public Animal 
{
public:
  void WhoAreYou()
  {
    std::cout << "I am a Bird." << std::endl;
  }
};

void DoSomething(Animal x)
{
  x.WhoAreYou();
}

int _tmain(int argc, _TCHAR* argv[])
{
  Animal animal;
  Bird bird;

  DoSomething(animal);
  DoSomething(bird);  

  return 0;
}

 

In both cases, a function call with base class and a function call with derived class, the output is: “I am an Animal”. As the function parameter expects an instance of an animal, this behavior is like expected. On function call the bird class instance will be used to create an animal class instance.

But what will happen if we don’t create a copy and pass a reference instead.

class Animal
{
public:
virtual void WhoAreYou()
{
std::cout << "I am an Animal." << std::endl;
}
};

class Bird : public Animal
{
public:
void WhoAreYou()
{
std::cout << "I am a Bird." <DoSomething();
}

MyOtherDerived* y2 = dynamic_cast(x);
if (y2 != nullptr)
{
y2->DoSomethingOther();
}
}

int _tmain(int argc, _TCHAR* argv[])
{
MyOtherDerived myOtherDerived;

Execute(&myOtherDerived);

return 0;
}

 

Beside the terrible variable names “y1” and “y2” the source code contains a big issue, something I want to call: “lying interfaces”. The function declaration for “Execute” tells everyone that the functions expects a parameter of type “MyBase”. But that’s a lie! The function expects a “MyDerived” object or a “MyOtherDerived” object. Because of public inheritance both classes are-a “MyBase” but not vice versa. So “MyBase” is never-ever a “MyDerived” or “MyOtherDerived”. Of course, with pointers and casting you can implement such functionality but beside the violation of the inheritance rules you will create “lying interfaces” which may result in big issues, for example source code which is hard to maintain or even worse, results in undefined behavior.

The same is true for return values. You should not return a base class pointer in case you want to return a derived class pointer. I have seen this issue several times, especially in interfaces which return data. In such cases there is one function which returns a pointer to a base class data type. But that’s a “lying interface” too as the function returns data of derived classes and the developer of the interfaces expects that the client will do casting and convert the type into the derived class. That’s a terrible software design which will move work and errors to the client. To avoid such “lying interfaces”, the interface must offer type specific versions of the functions or as a better solution, the software design for the data classes should be adapted to fulfill the needs of the interface and therefore the needs of the client.

 

Interfaces

Within the examples of the article we have seen base class and derived class objects and functions which expect base class parameters. A special kind of base class is the interface. In C++ you can interfaces by implementing pure virtual classes. I would to recommend the use of interfaces. If your base class is an interface, you can create a loose coupling. The function call parameter or return value as well as class members can be of the interface type instead of a base class type. As the interface is more generic and independent of a real implementation, you create a loose relationship between objects. You expect something which offers the needed functionality instead of a concrete implementation. This dependency against functionality allows a more efficient software design.

 

Summary

In C++ you can implement inheritance according to the object-oriented paradigm by public inherit derived classes from base classes. This relationship is always an “is-a” relationship which means: the derived class “is-a” base class but not vice versa. Therefore, whenever a function expects a base class as parameter you can pass a derived class too.

This inheritance mechanism is well supported by the C++ compilers. Issues will occur only in case software developers ignore the rules of the object-oriented paradigm. In my opinion, the most common errors are a violation of the Liskov Substitution Principle and “lying interfaces”. Such interfaces will force the interface implementer or the interface client to implement against some (maybe written down) expectations. Such lying interfaces can be identified very easily as type casting will become necessary on interface and/or client side.

Advertisements
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

2 Antworten zu Public inheritance always creates an “is-a” relationship

  1. Pingback: Private inheritance creates a “is implemented in terms of” relationship | coders corner

  2. Pingback: Public vs. protected vs. private inheritance | coders corner

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s