Calling Inherited Functions and Overriding Behavior

By default, derived classes inherit all of the behaviors defined in a base class. In this lesson, we'll examine in more detail how member functions are selected, as well as how we can leverage this to change behaviors in a derived class.

When a member function is called on a derived class object, the compiler first looks to see if any function with that name exists in the derived class. If so, all overloaded functions with that name are considered, and the function overload resolution process is used to determine whether there is a best match. If not, the compiler walks up the inheritance chain, checking each parent class in turn in the same way.

Put another way, the compiler will select the best matching function from the most-derived class with at least one function with that name.

Calling a base class function

First, let's explore what happens when the derived class has no matching function, but the base class does:

#include <iostream>

class Entity
{
public:
    Entity() { }

    void describe() const { std::cout << "Entity::describe()\n"; }
};

class Enemy: public Entity
{
public:
    Enemy() { }
};

int main()
{
    Entity entity{};
    entity.describe();

    Enemy enemy{};
    enemy.describe();

    return 0;
}

This prints:

Entity::describe() Entity::describe()

When entity.describe() is called, the compiler looks to see if a function named describe() has been defined in class Entity. It has, so the compiler looks to see if it is a match. It is, so it is called.

When enemy.describe() is called, the compiler looks to see if a function named describe() has been defined in the Enemy class. It hasn't. So it moves to the parent class (in this case, Entity), and tries again there. Entity has defined a describe() function, so it uses that one. In other words, Entity::describe() was used because Enemy::describe() doesn't exist.

This means that if the behavior provided by a base class is sufficient, we can simply use the base class behavior.

Redefining behaviors

However, if we had defined Enemy::describe() in the Enemy class, it would have been used instead.

This means that we can make functions work differently with our derived classes by redefining them in the derived class!

For example, let's say we want enemy.describe() to print Enemy::describe(). We can simply add function describe() in the Enemy class so it returns the correct response when we call function describe() with an Enemy object.

To modify the way a function defined in a base class works in the derived class, simply redefine the function in the derived class.

#include <iostream>

class Entity
{
public:
    Entity() { }

    void describe() const { std::cout << "Entity::describe()\n"; }
};

class Enemy: public Entity
{
public:
    Enemy() { }

    void describe() const { std::cout << "Enemy::describe()\n"; }
};

int main()
{
    Entity entity{};
    entity.describe();

    Enemy enemy{};
    enemy.describe();

    return 0;
}

This prints:

Entity::describe() Enemy::describe()

Note that when you redefine a function in the derived class, the derived function does not inherit the access specifier of the function with the same name in the base class. It uses whatever access specifier it is defined under in the derived class. Therefore, a function that is defined as private in the base class can be redefined as public in the derived class, or vice-versa!

#include <iostream>

class Entity
{
private:
    void showStats() const
    {
        std::cout << "Entity";
    }
};

class Enemy : public Entity
{
public:
    void showStats() const
    {
        std::cout << "Enemy";
    }
};

int main()
{
    Enemy enemy{};
    enemy.showStats(); // calls Enemy::showStats(), which is public
    return 0;
}

Adding to existing functionality

Sometimes we don't want to completely replace a base class function, but instead want to add additional functionality to it when called with a derived object. In the above example, note that Enemy::describe() completely hides Entity::describe()! This may not be what we want. It is possible to have our derived function call the base version of the function of the same name (in order to reuse code) and then add additional functionality to it.

To have a derived function call a base function of the same name, simply do a normal function call, but prefix the function with the scope qualifier of the base class. For example:

#include <iostream>

class Entity
{
public:
    Entity() { }

    void describe() const { std::cout << "Entity::describe()\n"; }
};

class Enemy: public Entity
{
public:
    Enemy() { }

    void describe() const
    {
        std::cout << "Enemy::describe()\n";
        Entity::describe(); // note call to Entity::describe() here
    }
};

int main()
{
    Entity entity{};
    entity.describe();

    Enemy enemy{};
    enemy.describe();

    return 0;
}

This prints:

Entity::describe() Enemy::describe() Entity::describe()

When enemy.describe() is executed, it resolves to Enemy::describe(). After printing Enemy::describe(), it then calls Entity::describe(), which prints Entity::describe().

This should be pretty straightforward. Why do we need to use the scope resolution operator (::)? If we had defined Enemy::describe() like this:

#include <iostream>

class Entity
{
public:
    Entity() { }

    void describe() const { std::cout << "Entity::describe()\n"; }
};

class Enemy: public Entity
{
public:
    Enemy() { }

    void describe() const
    {
        std::cout << "Enemy::describe()\n";
        describe(); // no scope resolution results in self-call and infinite recursion
    }
};

int main()
{
    Entity entity{};
    entity.describe();

    Enemy enemy{};
    enemy.describe();

    return 0;
}

Calling function describe() without a scope resolution qualifier would default to the describe() in the current class, which would be Enemy::describe(). This would cause Enemy::describe() to call itself, which would lead to an infinite recursion!

There's one bit of trickiness that we can run into when trying to call friend functions in base classes, such as operator<<. Because friend functions of the base class aren't actually part of the base class, using the scope resolution qualifier won't work. Instead, we need a way to make our Enemy class temporarily look like the Entity class so that the right version of the function can be called.

Fortunately, that's easy to do, using static_cast. Here's an example:

#include <iostream>

class Entity
{
public:
    Entity() { }

    friend std::ostream& operator<< (std::ostream& out, const Entity&)
    {
        out << "Entity info\n";
        return out;
    }
};

class Enemy: public Entity
{
public:
    Enemy() { }

    friend std::ostream& operator<< (std::ostream& out, const Enemy& e)
    {
        out << "Enemy info\n";
        // static_cast Enemy to an Entity object, so we call the right version of operator<<
        out << static_cast<const Entity&>(e);
        return out;
    }
};

int main()
{
    Enemy enemy{};

    std::cout << enemy << '\n';

    return 0;
}

Because an Enemy is-a Entity, we can static_cast our Enemy object into an Entity reference, so that the appropriate version of operator<< that uses an Entity is called.

This prints:

Enemy info Entity info

Overload resolution in derived classes

As noted at the top of the lesson, the compiler will select the best matching function from the most-derived class with at least one function with that name.

First, let's take a look at a simple case where we have overloaded member functions:

#include <iostream>

class Entity
{
public:
    void attack(int)    { std::cout << "Entity::attack(int)\n"; }
    void attack(double) { std::cout << "Entity::attack(double)\n"; }
};

class Enemy: public Entity
{
public:
};

int main()
{
    Enemy e{};
    e.attack(10); // calls Entity::attack(int)

    return 0;
}

For the call e.attack(10), the compiler doesn't find a function named attack() in Enemy, so it checks Entity where it finds two functions with that name. It uses the function overload resolution process to determine that Entity::attack(int) is a better match than Entity::attack(double). Therefore, Entity::attack(int) gets called, just like we'd expect.

Now let's look at a case that doesn't behave like we might expect:

#include <iostream>

class Entity
{
public:
    void attack(int)    { std::cout << "Entity::attack(int)\n"; }
    void attack(double) { std::cout << "Entity::attack(double)\n"; }
};

class Enemy: public Entity
{
public:
    void attack(double) { std::cout << "Enemy::attack(double)"; } // this function added
};

int main()
{
    Enemy e{};
    e.attack(10); // calls Enemy::attack(double), not Entity::attack(int)

    return 0;
}

For the call e.attack(10), the compiler finds one function named attack() in Enemy, therefore it will only consider functions in Enemy when trying to determine what function to resolve to. This function is also the best matching function in Enemy for this function call. Therefore, this calls Enemy::attack(double).

Since Entity::attack(int) has a parameter that is a better match for int argument 10 than Enemy::attack(double), you may have been expecting this function call to resolve to Entity::attack(int). But because e is an Enemy, there is at least one attack() function in Enemy, and Enemy is more derived than Entity, the functions in Entity are not even considered.

So what if we actually want e.attack(10) to resolve to Entity::attack(int)? One not-great way is to define an Enemy::attack(int):

#include <iostream>

class Entity
{
public:
    void attack(int)    { std::cout << "Entity::attack(int)\n"; }
    void attack(double) { std::cout << "Entity::attack(double)\n"; }
};

class Enemy: public Entity
{
public:
    void attack(int n) { Entity::attack(n); } // works but not great, as we have to define
    void attack(double) { std::cout << "Enemy::attack(double)"; }
};

int main()
{
    Enemy e{};
    e.attack(10); // calls Enemy::attack(int), which calls Entity::attack(int)

    return 0;
}

While this works, it's not great, as we have to add a function to Enemy for every overload we want to fall through to Entity. That could be a lot of extra functions that essentially just route calls to Entity.

A better option is to use a using-declaration in Enemy to make all Entity functions with a certain name visible from within Enemy:

#include <iostream>

class Entity
{
public:
    void attack(int)    { std::cout << "Entity::attack(int)\n"; }
    void attack(double) { std::cout << "Entity::attack(double)\n"; }
};

class Enemy: public Entity
{
public:
    using Entity::attack; // make all Entity::attack() functions eligible for overload resolution
    void attack(double) { std::cout << "Enemy::attack(double)"; }
};

int main()
{
    Enemy e{};
    e.attack(10); // calls Entity::attack(int), which is the best matching function visible in Enemy

    return 0;
}

By putting the using-declaration using Entity::attack; inside Enemy, we are telling the compiler that all Entity functions named attack should be visible in Enemy, which will cause them to be eligible for overload resolution. As a result, Entity::attack(int) is selected over Enemy::attack(double).

Summary

Function resolution in inheritance: When calling a member function on a derived class object, the compiler first checks the derived class for any function with that name. If found, it uses function overload resolution among those functions. If not found, it walks up the inheritance chain checking each parent class. The compiler selects the best matching function from the most-derived class with at least one function with that name.

Redefining base class functions: Derived classes can redefine functions from the base class by declaring a function with the same name. When called on a derived object, the derived version is used instead of the base version. The redefined function doesn't inherit the access specifier from the base class - it uses whatever access specifier it's defined under in the derived class.

Calling base class versions: To have a derived function call the base version of the same-named function, use the scope resolution operator: BaseClass::functionName(). This is useful for extending base functionality rather than completely replacing it. Without the scope qualifier, the function would call itself, causing infinite recursion.

Friend function handling: When calling friend functions from derived classes (like operator<<), the scope resolution operator doesn't work because friend functions aren't part of the class. Instead, use static_cast to temporarily treat the derived object as a base object: static_cast<const BaseClass&>(derivedObject).

Overload resolution behavior: When the derived class has any function with a given name, only functions from that class are considered for overload resolution, even if a base class function would be a better match. To make base class overloads visible for resolution alongside derived class functions, use a using-declaration: using BaseClass::functionName;.

Using-declarations for overloads: A using-declaration makes all base class functions with a specified name visible in the derived class for overload resolution. This allows base class overloads to be considered alongside derived class functions without having to manually forward each overload.

Understanding function resolution and overriding enables precise control over which version of a function is called, allowing derived classes to selectively inherit, extend, or replace base class behavior as needed.