Hiding Inherited Functionality

Changing an inherited member's access level

C++ gives us the ability to change an inherited member's access specifier in the derived class. This is done by using a using declaration to identify the (scoped) base class member that is having its access changed in the derived class, under the new access specifier.

For example, consider the following Entity class:

#include <iostream>

class Entity
{
private:
    int m_health{};

public:
    Entity(int health)
        : m_health{ health }
    {
    }

protected:
    void showHealth() const { std::cout << m_health; }
};

Because Entity::showHealth() has been declared as protected, it can only be called by Entity or its derived classes. The public cannot access it.

Let's define an Enemy class that changes the access specifier of showHealth() to public:

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

    // Entity::showHealth was inherited as protected, so the public has no access
    // But we're changing it to public via a using declaration
    using Entity::showHealth; // note: no parenthesis here
};

This means that this code will now work:

int main()
{
    Enemy goblin{ 50 };

    // showHealth is public in Enemy, so this is okay
    goblin.showHealth(); // prints 50
    return 0;
}

You can only change the access specifiers of base members the derived class would normally be able to access. Therefore, you can never change the access specifier of a base member from private to protected or public, because derived classes do not have access to private members of the base class.

Hiding functionality

In C++, it is not possible to remove or restrict functionality from a base class other than by modifying the source code. However, in a derived class, it is possible to hide functionality that exists in the base class, so that it cannot be accessed through the derived class. This can be done simply by changing the relevant access specifier.

For example, we can make a public member private:

#include <iostream>

class Entity
{
public:
    int m_health{};
};

class Enemy : public Entity
{
private:
    using Entity::m_health;

public:
    Enemy(int health) : Entity{ health }
    {
    }
};

int main()
{
    Enemy goblin{ 50 };
    std::cout << goblin.m_health; // error: m_health is private in Enemy

    Entity& entity{ goblin };
    std::cout << entity.m_health; // okay: m_health is public in Entity

    return 0;
}

This allowed us to take a poorly designed base class and encapsulate its data in our derived class. Alternatively, instead of inheriting Entity's members publicly and making m_health private by overriding its access specifier, we could have inherited Entity privately, which would have caused all of Entity's members to be inherited privately in the first place.

Warning
Hiding inherited functionality can violate the Liskov Substitution Principle (LSP), which states that objects of a derived class should be usable anywhere the base class is expected. If you hide or delete base class functionality, code that works with the base class may fail or behave unexpectedly with the derived class.
Key Concept
If you find yourself frequently hiding inherited functionality, it may indicate a design problem. Consider whether inheritance is the right relationship, or whether composition would be more appropriate. A derived class that removes functionality isn't truly "is-a" the base class.

However, it is worth noting that while m_health is private in the Enemy class, it is still public in the Entity class. Therefore the encapsulation of m_health in Enemy can still be subverted by casting to Entity& and directly accessing the member.

Advanced note: For the same reason, if an Entity class has a public virtual function, and the Enemy class changes the access specifier to private, the public can still access the private Enemy function by casting an Enemy object to an Entity& and calling the virtual function. The compiler will allow this because the function is public in Entity. However, because the object is actually an Enemy, virtual function resolution will resolve to (and call) the (private) Enemy version of the function. Access controls are not enforced at runtime.

#include <iostream>

class GameUnit
{
public:
    virtual void execute()
    {
        std::cout << "public GameUnit::execute()\n";
    }
};

class SpecialUnit : public GameUnit
{
private:
    virtual void execute()
    {
         std::cout << "private SpecialUnit::execute()\n";
   }
};

int main()
{
    SpecialUnit unit{};
    unit.execute();                  // compile error: not allowed as SpecialUnit::execute() is private
    static_cast<GameUnit&>(unit).execute(); // okay: GameUnit::execute() is public, resolves to private SpecialUnit::execute() at runtime

    return 0;
}

Perhaps surprisingly, given a set of overloaded functions in the base class, there is no way to change the access specifier for a single overload. You can only change them all:

#include <iostream>

class Entity
{
public:
    int m_health{};

    int getHealth() const { return m_health; }
    int getHealth(int) const { return m_health; }
};

class Enemy : public Entity
{
private:
    using Entity::getHealth; // make ALL getHealth functions private

public:
    Enemy(int health) : Entity{ health }
    {
    }
};

int main()
{
    Enemy goblin{ 50 };
    std::cout << goblin.getHealth();  // error: getHealth() is private in Enemy
    std::cout << goblin.getHealth(5); // error: getHealth(int) is private in Enemy

    return 0;
}

Deleting functions in the derived class

You can also mark member functions as deleted in the derived class, which ensures they can't be called at all through a derived object:

#include <iostream>

class Entity
{
private:
    int m_health{};

public:
    Entity(int health)
        : m_health{ health }
    {
    }

    int getHealth() const { return m_health; }
};

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

    int getHealth() const = delete; // mark this function as inaccessible
};

int main()
{
    Enemy goblin{ 50 };

    // The following won't work because getHealth() has been deleted!
    std::cout << goblin.getHealth();

    return 0;
}

In the above example, we've marked the getHealth() function as deleted. This means that the compiler will complain when we try to call the derived version of the function. Note that the Entity version of getHealth() is still accessible though. We can call Entity::getHealth() in one of two ways:

int main()
{
    Enemy goblin{ 50 };

    // We can call the Entity::getHealth() function directly
    std::cout << goblin.Entity::getHealth();

    // Or we can upcast Enemy to an Entity reference and getHealth() will resolve to Entity::getHealth()
    std::cout << static_cast<Entity&>(goblin).getHealth();

    return 0;
}

If using the casting method, we cast to an Entity& rather than an Entity to avoid making a copy of the Entity portion of goblin.

Summary

Changing access specifiers: Using-declarations allow derived classes to change the access level of inherited members. The syntax using BaseClass::memberName; placed under a different access specifier in the derived class changes how that member can be accessed through the derived class. However, you can only change access for members the derived class can normally access (you cannot change private base members).

Hiding functionality: While C++ doesn't allow removing functionality from a base class, derived classes can hide inherited functionality by changing access specifiers. For example, making a public base member private in the derived class prevents external code from accessing it through the derived class, though the encapsulation can be bypassed by casting to the base class type.

Changing all overloads: When using a using-declaration to change access for overloaded functions, all overloads of that function name change access together. There's no way to selectively change access for individual overloads while leaving others unchanged.

Deleting functions: Derived classes can mark inherited member functions as deleted using = delete, preventing them from being called through derived class objects. The base class version remains accessible by explicitly calling BaseClass::function() or by upcasting the derived object to a base class reference.

Access control limitations: Access controls are enforced at compile time, not runtime. Changing an inherited member's access specifier only affects how that member is accessed through the derived class. The base class access specifier still applies when accessing the member through a base class pointer or reference, allowing encapsulation to be subverted through casting.

Using these techniques allows derived classes to expose, hide, or delete inherited functionality as needed, though the ability to bypass these restrictions through casting means they provide compile-time safety rather than true runtime encapsulation.

Best Practice
Before hiding inherited functionality, ask yourself: "Does this derived class truly have an is-a relationship with the base class?" If not, consider private inheritance or composition instead. Use hiding sparingly and only when fixing poorly designed base classes you cannot modify.