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 Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
This prints:
Base::identify() Base::identify()
When base.identify()
is called, the compiler looks to see if a function named identify()
has been defined in class Base
. It has, so the compiler looks to see if it is a match. It is, so it is called
When derived.identify()
is called, the compiler looks to see if a function named identify()
has been defined in the Derived
class. It hasn’t. So it moves to the parent class (in this case, Base
), and tries again there. Base
has defined an identify()
function, so it uses that one. In other words, Base::identify()
was used because Derived::identify()
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 Derived::identify()
in the Derived
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 derived.identify()
to print Derived::identify()
. We can simply add function identify()
in the Derived
class so it returns the correct response when we call function identify()
with a Derived
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 Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
void identify() const { std::cout << "Derived::identify()\n"; }
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
This prints:
Base::identify() Derived::identify()
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 Base
{
private:
void print() const
{
std::cout << "Base";
}
};
class Derived : public Base
{
public:
void print() const
{
std::cout << "Derived ";
}
};
int main()
{
Derived derived {};
derived.print(); // calls derived::print(), 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 Derived::identify()
completely hides Base::identify()
! 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 Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
void identify() const
{
std::cout << "Derived::identify()\n";
Base::identify(); // note call to Base::identify() here
}
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
This prints:
Base::identify() Derived::identify() Base::identify()
When derived.identify()
is executed, it resolves to Derived::identify()
. After printing Derived::identify()
, it then calls Base::identify()
, which prints Base::identify()
.
This should be pretty straightforward. Why do we need to use the scope resolution operator (::)? If we had defined Derived::identify()
like this:
#include <iostream>
class Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
void identify() const
{
std::cout << "Derived::identify()\n";
identify(); // no scope resolution results in self-call and infinite recursion
}
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
Calling function identify()
without a scope resolution qualifier would default to the identify()
in the current class, which would be Derived::identify()
. This would cause Derived::identify()
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 Derived
class temporarily look like the Base
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 Base
{
public:
Base() { }
friend std::ostream& operator<< (std::ostream& out, const Base&)
{
out << "In Base\n";
return out;
}
};
class Derived: public Base
{
public:
Derived() { }
friend std::ostream& operator<< (std::ostream& out, const Derived& d)
{
out << "In Derived\n";
// static_cast Derived to a Base object, so we call the right version of operator<<
out << static_cast<const Base&>(d);
return out;
}
};
int main()
{
Derived derived {};
std::cout << derived << '\n';
return 0;
}
Because a Derived
is-a Base, we can static_cast
our Derived
object into a Base
reference, so that the appropriate version of operator<<
that uses a Base
is called.
This prints:
In Derived In Base
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 Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
};
int main()
{
Derived d{};
d.print(5); // calls Base::print(int)
return 0;
}
For the call d.print(5)
, the compiler doesn’t find a function named print()
in Derived
, so it checks Base
where it finds two functions with that name. It uses the function overload resolution process to determine that Base::print(int)
is a better match than Base::print(double)
. Therefore, Base::print(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 Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
void print(double) { std::cout << "Derived::print(double)"; } // this function added
};
int main()
{
Derived d{};
d.print(5); // calls Derived::print(double), not Base::print(int)
return 0;
}
For the call d.print(5)
, the compiler finds one function named print()
in Derived
, therefore it will only consider functions in Derived
when trying to determine what function to resolve to. This function is also the best matching function in Derived
for this function call. Therefore, this calls Derived::print(double)
.
Since Base::print(int)
has a parameter that is a better match for int argument 5
than Derived::print(double)
, you may have been expecting this function call to resolve to Base::print(int)
. But because d
is a Derived
, there is at least one print()
function in Derived
, and Derived
is more derived than Base
, the functions in Base
are not even considered.
So what if we actually want d.print(5)
to resolve to Base::print(int)
? One not-great way is to define a Derived::print(int)
:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
void print(int n) { Base::print(n); } // works but not great, as we have to define
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d{};
d.print(5); // calls Derived::print(int), which calls Base::print(int)
return 0;
}
While this works, it’s not great, as we have to add a function to Derived
for every overload we want to fall through to Base
. That could be a lot of extra functions that essentially just route calls to Base
.
A better option is to use a using-declaration in Derived
to make all Base
functions with a certain name visible from within Derived
:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
using Base::print; // make all Base::print() functions eligible for overload resolution
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d{};
d.print(5); // calls Base::print(int), which is the best matching function visible in Derived
return 0;
}
By putting the using-declaration using Base::print;
inside Derived
, we are telling the compiler that all Base
functions named print
should be visible in Derived
, which will cause them to be eligible for overload resolution. As a result, Base::print(int)
is selected over Derived::print(double)
.