Inheritance

Inheritance lets one class build on another: the derived class gets the base members and adds its own. It is the mechanism behind every interface in the standard library and behind most of the design patterns in part 11.

The mechanics are small: a base class in the declaration, a base constructor in the initialiser list, and an access level that decides what the derived class may touch. The traps are smaller and quieter: names that hide other names, and objects that lose their derived part when copied into a base.

A derived class

The declaration class savings : public account makes savings a kind of account: it contains an account subobject and may use its public members. The base constructor is called in the derived initialiser list.

The program creates a savings object and calls a function inherited from the base. At() and deposit() were written once, in account, and the derived class did not repeat them.

Public inheritance means "is a": a savings can be used anywhere an account is expected. That sentence is the whole design rule, and the last section shows what happens when it is ignored.

The base class needs a virtual destructor before anything is deleted through a pointer to it; lesson 035 shows why, and the next section shows the constructor that runs first.

#include <iostream> class account { public: explicit account(int balance) : balance_(balance) {} void deposit(int amount) { balance_ += amount; } int balance() const { return balance_; } private: int balance_; }; class savings : public account { public: savings(int balance, int rate) : account(balance), rate_(rate) {} int rate() const { return rate_; } private: int rate_; }; int main() { savings s{100, 3}; s.deposit(50); std::cout << s.balance() << ' ' << s.rate() << '\n'; }

g++ -std=c++23 -Wall -Wextra derive.cpp -o derive
./derive

150 3

protected

A protected member is visible to the class and to classes derived from it, and invisible to everybody else. It is the access level for state that a derived class has to maintain but callers must not touch.

The program's base keeps balance_ protected so withdraw() in the derived class can change it. main cannot, and neither can a free function.

Use protected sparingly: every derived class becomes part of the base's implementation contract, and a change to the protected member breaks them all. Private data with protected functions is the narrower design.

#include <iostream> class account { public: explicit account(int balance) : balance_(balance) {} int balance() const { return balance_; } protected: int balance_; }; class savings : public account { public: savings(int balance, int rate) : account(balance), rate_(rate) {} void add_interest() { balance_ += balance_ * rate_ / 100; } private: int rate_; }; int main() { savings s{200, 5}; s.add_interest(); std::cout << s.balance() << '\n'; }

g++ -std=c++23 -Wall -Wextra protected.cpp -o protected_demo
./protected_demo

210

Derived names hide base names

A function declared in the derived class hides every function of the same name in the base, even the overloads with different parameters. Calling the hidden overload through a derived object is then an error.

The program's derived class adds log(int), and the base has log() and log(const char*). After the declaration of log(int), the other two are no longer found through derived, and the call log("text") fails to compile.

The fix is using base::log; in the derived class, which brings the base overloads back into the same scope. It is one line and it is easy to forget, which is why the error message names a conversion that looks impossible.

#include <iostream> class base { public: void log() { std::cout << "base log\n"; } void log(const char* text) { std::cout << "base log " << text << '\n'; } }; class derived : public base { public: void log(int value) { std::cout << "derived log " << value << '\n'; } }; int main() { derived d; d.log(3); d.log("text"); }

g++ -std=c++23 -Wall -Wextra hiding.cpp -o hiding

hiding.cpp: In function 'int main()':
hiding.cpp:30:11: error: invalid conversion from 'const char*' to 'int' [-fpermissive]
30 | d.log("text");
| ^~~~~~
| |
| const char*
hiding.cpp:20:18: note: initializing argument 1 of 'void derived::log(int)'
20 | void log(int value)
| ~~~~^~~~~

Object slicing

Passing a derived object by value to a function that takes a base copies only the base part. The derived members are cut off, and the function works on a complete base object that is not the original.

The program's describe takes an account by value and prints its balance. The savings object passed to it is sliced: rate_ is gone, and any behaviour that depended on it cannot be reached.

Take the base by reference or pointer instead, and the object stays whole. If the function has to call behaviour that differs per derived type, it needs a virtual function, which is the next lesson.

#include <iostream> class account { public: explicit account(int balance) : balance_(balance) {} int balance() const { return balance_; } private: int balance_; }; class savings : public account { public: savings(int balance, int rate) : account(balance), rate_(rate) {} int rate() const { return rate_; } private: int rate_; }; void describe(account a) { std::cout << "balance " << a.balance() << '\n'; } int main() { savings s{100, 3}; describe(s); std::cout << "sizeof account: " << sizeof(account) << '\n'; std::cout << "sizeof savings: " << sizeof(savings) << '\n'; }

g++ -std=c++23 -Wall -Wextra slicing.cpp -o slicing
./slicing

balance 100
sizeof account: 4
sizeof savings: 8

Base first, base last

Construction runs from the base to the derived, and destruction runs the other way. The base subobject is complete before the derived constructor body starts, which is why the base constructor must run first.

The program prints a line from each constructor and each destructor. The base is constructed first and destroyed last, and the derived lines sit inside them like brackets.

The order matters when a derived constructor uses a base member: the member exists by then. It also explains why a virtual call in a base constructor reaches the base version, a rule that lesson 035 returns to.

#include <iostream> class base { public: base() { std::cout << "construct base\n"; } ~base() { std::cout << "destroy base\n"; } }; class derived : public base { public: derived() { std::cout << "construct derived\n"; } ~derived() { std::cout << "destroy derived\n"; } }; int main() { derived d; std::cout << "body\n"; }

g++ -std=c++23 -Wall -Wextra order.cpp -o order
./order

construct base
construct derived
body
destroy derived
destroy base

Exercises

  1. Add using base::log; to derived in hiding.cpp and check that both base overloads work again.
  2. Change describe in slicing.cpp to take const account& and print sizeof of the parameter inside the function.
  3. Make savings private-inherit from account and see which calls in derive.cpp stop compiling.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a derived class get from its base?

    Answer

    The base members, with the access the base allows

    The base subobject is part of the derived object, and the derived class can use what is accessible.

  2. What does protected allow?

    Answer

    Access from the class and its derived classes

    Protected is the middle level: the derived class sees the member, the outside world does not.

  3. What happens when a derived class declares a function with the same name as a base one?

    Answer

    The derived name hides the whole base overload set

    Name lookup stops at the derived class, and a using declaration is needed to bring the base names back.

  4. What is object slicing?

    Answer

    Copying a derived object into a base object loses the derived part

    The base subobject is copied and the derived members are left behind.

  5. What does this program print? #include <iostream> class base { public: int balance() const { return balance_; } protected: int balance_ = 150; }; class savings : public base { public: int extra() const { return bonus_; } private: int bonus_ = 3; }; int main() { savings s; std::cout << s.balance() << ' ' << s.extra(); }

    Answer

    150 3

    The derived object has the base member and its own, and the program prints both.

  6. What does this program print? #include <iostream> class base { protected: int value_ = 200; }; class derived : public base { public: int total() const { return value_ + 10; } }; int main() { derived d; std::cout << d.total(); }

    Answer

    210

    The derived class reads the protected member and computes 200 plus 10.

  7. What does d.log("text") report when the derived class hides the base overload?

    Answer

    invalid conversion from 'const char*' to 'int'

    The derived log(int) hides the base log(const char*), and the literal cannot convert to int.

  8. What does this program print? #include <iostream> class account { public: int balance = 100; }; class savings : public account { public: int rate = 2; }; int main() { savings s; account a = s; std::cout << "balance " << a.balance << " / sizeof account: " << sizeof(account) << " / sizeof savings: " << sizeof(savings); }

    Answer

    balance 100 / sizeof account: 4 / sizeof savings: 8

    The base object holds only the balance, and the derived one adds a member, so their sizes differ.

  9. What is the order of the lines? #include <iostream> struct base { base() { std::cout << "construct base\n"; } ~base() { std::cout << "destroy base\n"; } }; struct derived : base { derived() { std::cout << "construct derived\n"; } ~derived() { std::cout << "destroy derived\n"; } }; int main() { derived d; std::cout << "body\n"; }

    Answer

    construct base / construct derived / body / destroy derived / destroy base

    The base subobject is built first and destroyed last, around the derived object.

  10. Why is the base constructed first?

    Answer

    The derived class needs a complete base subobject before its own members

    The derived constructor can use the base part, so it must already exist.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image