C++ Module 8 – Inheritance and polymorphism
Inheritance in C++: base and derived classes
Derive a C++ class from a base class, predict the order of construction and destruction, and fix name hiding, with every example built by Clang and GCC.
What you will learn
- Derive a class from a base class with public inheritance
- Predict the order in which a derived object is constructed and destroyed
- Fix name hiding in a derived class with a using-declaration
- Protect a base-class invariant with private data and protected functions
- Decide when public inheritance fits and when a member is the better design
Before you start
What you’ll learn · Before you start
What you will learn
- Derive a class from a base class with public inheritance
- Predict the order in which a derived object is constructed and destroyed
- Fix name hiding in a derived class with a using-declaration
- Protect a base-class invariant with private data and protected functions
- Decide when public inheritance fits and when a member is the better design
Before you start
On this page
class Circle : public Shape puts a complete Shape inside every Circle, 24 of its 32 bytes with Apple clang, so
every function written for a Shape& accepts a circle as well. The same mechanism can change a call nobody touched:
give a derived class one new print(long), and p.print(255) prints hex 0xff instead of int 255, with no warning
from either compiler.
You will derive a first class, step through the exact order in which the parts of a derived object are built and destroyed, reproduce that silent change and fix it with one line, keep a base class’s rules safe from its derived classes, and decide when deriving is the wrong tool. Every example is built and run with Apple clang 21 and GCC 16.
A derived object contains a complete base object
Inheritance lets one class build on another. In class Circle : public Shape { … };, Shape is the
base class: The class another class derives from: in class Circle : public Shape { … };, Shape is the base class. Every object of the derived class contains a complete object of the base, its base class subobject. and Circle is the derived class: A class declared with a base class after a colon, as in class Circle : public Shape. It has every member of its base plus its own, and with public inheritance it can be used wherever a reference or a pointer to the base is expected.. Every
Circle object contains a complete Shape inside it, its base class subobject: The complete base class object inside every object of a derived class. It is built before the derived class's own members and destroyed after them, and a reference or a pointer to the base refers to it.,
followed by the members that Circle adds. The public members of Shape stay public in Circle, so code can call
wheel.name() on a circle as if Circle had declared name() itself, and a Circle can be passed wherever a
Shape& or a Shape* is expected:
// Circle derives from Shape: a Circle has everything a Shape has, plus a radius, and it can be used wherever a
// Shape is expected.
#include <numbers>
#include <print>
#include <string>
#include <utility>
class Shape {
public:
explicit Shape(std::string name) : name_(std::move(name)) {}
const std::string& name() const { return name_; }
void rename(std::string name) { name_ = std::move(name); }
private:
std::string name_;
};
class Circle : public Shape {
public:
// The base part is built first, by the Shape constructor named in the member initialiser list.
Circle(std::string name, double radius) : Shape(std::move(name)), radius_(radius) {}
double radius() const { return radius_; }
double area() const { return std::numbers::pi * radius_ * radius_; }
private:
double radius_;
};
// Takes any Shape, so a Circle works too: the reference refers to the Shape part inside the Circle.
void print_name(const Shape& shape) {
std::println("print_name sees \"{}\"", shape.name());
}
int main() {
Circle wheel{"wheel", 0.35};
wheel.rename("front wheel"); // a member function inherited from Shape
std::println("{}: radius {} m, area {:.4f} m²", wheel.name(), wheel.radius(), wheel.area());
print_name(wheel);
Shape* any = &wheel; // a Circle* converts to a Shape* without a cast
std::println("through a Shape*: {}", any->name());
std::println("sizeof(Shape) {}, sizeof(Circle) {}", sizeof(Shape), sizeof(Circle));
} from session import run
# shapes.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra shapes.cpp -o shapes && ./shapes
g++-16 -std=c++23 -Wall -Wextra shapes.cpp -o shapes-gcc && ./shapes-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra shapes.cpp -o shapes && ./shapes front wheel: radius 0.35 m, area 0.3848 m² print_name sees "front wheel" through a Shape*: front wheel sizeof(Shape) 24, sizeof(Circle) 32 $ g++-16 -std=c++23 -Wall -Wextra shapes.cpp -o shapes-gcc && ./shapes-gcc front wheel: radius 0.35 m, area 0.3848 m² print_name sees "front wheel" through a Shape*: front wheel sizeof(Shape) 32, sizeof(Circle) 40
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shapes_session.py
Circle declares only a constructor, radius() and area(). The calls wheel.rename(…) and wheel.name() work
because a derived class inherits the members of its base, and with public inheritance the public members of Shape
are public members of Circle too.
The constructor shows how the base part gets its value. A derived class cannot initialise the base’s members one by
one; its member initialiser list: The part of a constructor after the colon, as in Percent(int value) : value_{value} {}. It initialises the named members before the body starts, in the order the members are declared, not the order of the list. More in “Constructors, member initialiser lists and explicit” names a base-class constructor instead,
here Shape(std::move(name)), and then its own members. The private member name_ is still inside every Circle,
but only Shape’s own functions may use it, so Circle changes the name through rename(), exactly like any other
code.
The last lines show what “contains a Shape” means in bytes. Shape is 24 bytes with libc++ and 32 with libstdc++,
which is the size of one std::string in each library, and Circle adds its 8-byte double. When print_name()
takes a const Shape&, the reference refers to the Shape part inside wheel. Nothing is copied, and the same
happens for the Shape* that &wheel converts to without a cast. In the drawing of wheel below, follow the
reference of print_name() into the object: it ends at the Shape part, the part that is built first and destroyed
last.
A derived object contains its base class as a sub-object
Text description of the diagram
The diagram shows the object wheel from shapes.cpp, a Circle, from top to bottom.
- At the top, the parameter const Shape& shape of print_name() has an arrow into the Circle object, to its Shape part: when a Circle is passed to a function that takes a Shape, the reference refers to the Shape sub-object inside the Circle, and nothing is copied.
- Inside the box for the Circle object, the first part is the Shape sub-object, which holds name_ with the value "front wheel". It is built first and destroyed last.
- An arrow labelled "then" leads to the second part, Circle's own member radius_ with the value 0.35. It is built after the Shape part and destroyed before it.
- Below the object, sizeof(Circle) is 32 bytes with libc++, the library of Apple clang, and 40 bytes with libstdc++, the library of GCC, because the two libraries make std::string 24 and 32 bytes long.
Through that reference, print_name() sees only what Shape declares: radius_ is in the object, after the Shape
part, but out of the function’s sight.
Checkpoint
Check: A derived object holds its base
A derived class must build its base and inherit it publicly
The constructor and the word after the colon are where a first derived class usually goes wrong. Each file below makes one of the two common mistakes:
C++ (no_base_constructor.cpp) · no_base_constructor.cpp
// Account has no default constructor, and SavingsAccount's constructor does not say which one to call.
#include <string>
#include <utility>
class Account {
public:
explicit Account(std::string owner) : owner_(std::move(owner)) {}
const std::string& owner() const { return owner_; }
private:
std::string owner_;
};
class SavingsAccount : public Account {
public:
explicit SavingsAccount(int rate_bp) : rate_bp_(rate_bp) {} // which Account constructor?
int rate_bp() const { return rate_bp_; }
private:
int rate_bp_;
};
int main() {
SavingsAccount s{350};
return s.rate_bp() == 350 ? 0 : 1;
} C++ (forgot_public.cpp) · forgot_public.cpp
// The keyword public is missing before Account, so SavingsAccount inherits privately: outside the class, nobody
// may treat a SavingsAccount as an Account.
#include <print>
class Account {
public:
long balance() const { return 50'000; }
};
class SavingsAccount : Account {};
void show_balance(const Account& account) { std::println("{}", account.balance()); }
int main() {
SavingsAccount s;
show_balance(s);
} from session import run
# Two mistakes, each built with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra -c no_base_constructor.cpp
g++-16 -std=c++23 -Wall -Wextra -c no_base_constructor.cpp
clang++ -std=c++23 -Wall -Wextra -c forgot_public.cpp
g++-16 -std=c++23 -Wall -Wextra -c forgot_public.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c no_base_constructor.cpp
no_base_constructor.cpp:16:14: error: constructor for 'SavingsAccount' must explicitly initialize the base class 'Account' which does not have a default constructor
16 | explicit SavingsAccount(int rate_bp) : rate_bp_(rate_bp) {} // which Account constructor?
| ^
no_base_constructor.cpp:5:7: note: 'Account' declared here
5 | class Account {
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c no_base_constructor.cpp
no_base_constructor.cpp: In constructor 'SavingsAccount::SavingsAccount(int)':
no_base_constructor.cpp:16:60: error: no matching function for call to 'Account::Account()'
16 | explicit SavingsAccount(int rate_bp) : rate_bp_(rate_bp) {} // which Account constructor?
| ^
• there are 3 candidates
• candidate 1: 'Account::Account(std::string)'
no_base_constructor.cpp:7:14:
7 | explicit Account(std::string owner) : owner_(std::move(owner)) {}
| ^~~~~~~
• candidate expects 1 argument, 0 provided
• candidate 2: 'constexpr Account::Account(const Account&)'
no_base_constructor.cpp:5:7:
5 | class Account {
| ^~~~~~~
• candidate expects 1 argument, 0 provided
• candidate 3: 'constexpr Account::Account(Account&&)'
• candidate expects 1 argument, 0 provided
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra -c forgot_public.cpp
forgot_public.cpp:16:18: error: cannot cast 'const SavingsAccount' to its private base class 'const Account'
16 | show_balance(s);
| ^
forgot_public.cpp:10:24: note: implicitly declared private here
10 | class SavingsAccount : Account {};
| ^~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c forgot_public.cpp
forgot_public.cpp: In function 'int main()':
forgot_public.cpp:16:17: error: 'Account' is an inaccessible base of 'SavingsAccount'
16 | show_balance(s);
| ~~~~~~~~~~~~^~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 base_errors_session.py
In the first file, Account has no default constructor and SavingsAccount’s constructor does not say which
Account constructor builds its base part. Clang states the rule in one sentence. GCC 16 reports that nothing matches
the call it would need, Account::Account(), and then lists the three constructors that do exist. The fix is to name
one: SavingsAccount(std::string owner, int rate_bp) : Account(std::move(owner)), rate_bp_(rate_bp) {}.
In the second file the word public is missing. A base class is private by default when the derived class is declared
with the keyword class, and public when it is declared with struct. With private inheritance, code outside
SavingsAccount may not treat it as an Account, so the call show_balance(s) fails: “cannot cast to its private
base class” in Clang’s words, “an inaccessible base” in GCC’s. The table sums up the rule for both keywords:
| Criterion | Declared with class | Declared with struct |
|---|---|---|
| No word before the base | A private base | A public base |
| show_balance(s) from main | Error: an inaccessible base | Compiles |
| public before the base | A public base | A public base |
The last row is the habit to keep. Private inheritance has rare uses, but a class that should be an Account says
public explicitly, whichever keyword declares it, and the compilers then check every use for you.
Checkpoint
Check: Building the base, inheriting publicly
Bases are built first and destroyed last
The standard fixes the order in which the parts of an object are built. First the base classes, in the order they are
listed after the colon; then the members, in the order they are declared in the class; and only then the body of the
constructor. The same rule holds inside the base class, so the members of Shape come before Shape’s constructor
body. Destruction runs the same steps in reverse: the destructor body of the derived class, its members, and then the
base part, whose own destructor body runs before its members are destroyed.
This program prints every step. Its member initialiser list names centre_ before Shape(), on purpose:
// Prints every step of creating and destroying a Circle, which derives from Shape. Tracer is a small member type
// that reports when it is constructed and destroyed.
#include <print>
#include <string>
#include <utility>
class Tracer {
public:
explicit Tracer(std::string name) : name_(std::move(name)) { std::println(" construct member {}", name_); }
~Tracer() { std::println(" destroy member {}", name_); }
private:
std::string name_;
};
class Shape {
public:
Shape() { std::println(" Shape constructor body"); }
~Shape() { std::println(" Shape destructor body"); }
private:
Tracer label_{"Shape::label_"};
};
class Circle : public Shape {
public:
// On purpose, the list names centre_ before Shape. The base is still built first, and -Wall says so.
explicit Circle(double radius) : centre_("Circle::centre_"), Shape(), radius_(radius) {
std::println(" Circle constructor body, radius {}", radius_);
}
~Circle() { std::println(" Circle destructor body"); }
private:
Tracer centre_;
double radius_;
};
int main() {
std::println("creating a Circle:");
{
Circle c{2.5};
std::println("using it");
} // c's lifetime ends here
std::println("done");
} from session import run
# order.cpp built with -Wall by both compilers: first the warnings about the initialiser list, then what it prints.
run("""
clang++ -std=c++23 -Wall -Wextra order.cpp -o order && ./order
g++-16 -std=c++23 -Wall -Wextra order.cpp -o order-gcc && ./order-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra order.cpp -o order && ./order
order.cpp:28:38: warning: field 'centre_' will be initialized after base 'Shape' [-Wreorder-ctor]
28 | explicit Circle(double radius) : centre_("Circle::centre_"), Shape(), radius_(radius) {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~
| Shape() centre_("Circle::centre_")
1 warning generated.
creating a Circle:
construct member Shape::label_
Shape constructor body
construct member Circle::centre_
Circle constructor body, radius 2.5
using it
Circle destructor body
destroy member Circle::centre_
Shape destructor body
destroy member Shape::label_
done
$ g++-16 -std=c++23 -Wall -Wextra order.cpp -o order-gcc && ./order-gcc
order.cpp: In constructor 'Circle::Circle(double)':
order.cpp:34:12: warning: 'Circle::centre_' will be initialized after [-Wreorder]
34 | Tracer centre_;
| ^~~~~~~
order.cpp:28:89: warning: base 'Shape' [-Wreorder]
28 | explicit Circle(double radius) : centre_("Circle::centre_"), Shape(), radius_(radius) {
| ^
order.cpp:28:14: warning: when initialized here [-Wreorder]
28 | explicit Circle(double radius) : centre_("Circle::centre_"), Shape(), radius_(radius) {
| ^~~~~~
creating a Circle:
construct member Shape::label_
Shape constructor body
construct member Circle::centre_
Circle constructor body, radius 2.5
using it
Circle destructor body
destroy member Circle::centre_
Shape destructor body
destroy member Shape::label_
done
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py
Step through the same run and watch each part of c being built and then destroyed, in the order the output printed
them:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- The list says centre_ first. Circle c{2.5}; calls Circle's constructor. Its list names centre_ before Shape(), and both compilers warned that it will not run that way: Clang with -Wreorder-ctor, GCC with -Wreorder.
- The base part first. The base class comes first, and inside it the members come before the body: the first line printed is construct member Shape::label_.
- Then the base's body. Shape's constructor body runs once all of Shape's members exist: Shape constructor body. The base part is now complete.
- Then Circle's members. Only now are Circle's own members built, in the order the class declares them: construct member Circle::centre_, although the list named it first.
- Then Circle's body. The derived constructor's body runs last, with every part ready: Circle constructor body, radius 2.5. Then main prints using it.
- Destruction starts at the top. At the closing brace, destruction runs the same steps backwards. Circle's destructor body runs first, while every member and the base part are still alive: Circle destructor body.
- Circle's members go. Then Circle's members are destroyed: destroy member Circle::centre_.
- Then the base part. The base part goes after everything Circle added, and inside it the body runs first: Shape destructor body.
- The first part is the last. Shape's member goes last: destroy member Shape::label_. The part built first is destroyed last, whichever constructor ran and whatever its list said.
Both compilers warn that the list does not follow the real order: Clang with -Wreorder-ctor and GCC with -Wreorder,
both turned on by -Wall. The output then shows that the list made no difference. Shape::label_ and the body of
Shape’s constructor come before Circle::centre_, because the order comes from the class definition, never from
the list. A class has one destructor but may have several constructors, and destruction must undo construction in
reverse whichever constructor ran, so only the declarations can decide.
Two practical rules follow from this order:
- A derived class’s member initialisers may use the base part, which is already complete. The base constructor can never use the derived members, because they do not exist yet.
- In the derived destructor, the base part is still alive. In the base destructor, the derived members are already gone. The next lesson shows what this means for virtual functions called from constructors and destructors.
Key idea
The class definition fixes the order: base classes, then members in declaration order, then the constructor’s body, and destruction runs the same steps backwards, whatever the member initialiser list says.
Checkpoint
Check: Construction and destruction order
One declaration hides the whole overload set
Printer has two print functions, and HexPrinter adds a third one with a long parameter. That looks like
overloading, but it is not:
C++ (hiding.cpp) · hiding.cpp
// HexPrinter adds one print() of its own. That one declaration hides both print() functions of Printer.
#include <print>
#include <string>
class Printer {
public:
void print(int value) const { std::println("int {}", value); }
void print(const std::string& text) const { std::println("text \"{}\"", text); }
};
class HexPrinter : public Printer {
public:
void print(long value) const { std::println("hex {:#x}", value); }
};
int main() {
HexPrinter p;
p.print(255); // Printer::print(int) would match exactly, but it is never considered
#ifdef WITH_TEXT
p.print(std::string("ready"));
#endif
} C++ (hiding_fixed.cpp) · hiding_fixed.cpp
// The using-declaration brings Printer's print() functions into HexPrinter, so all three take part in overload
// resolution and the best match wins.
#include <print>
#include <string>
class Printer {
public:
void print(int value) const { std::println("int {}", value); }
void print(const std::string& text) const { std::println("text \"{}\"", text); }
};
class HexPrinter : public Printer {
public:
using Printer::print;
void print(long value) const { std::println("hex {:#x}", value); }
};
int main() {
HexPrinter p;
p.print(255);
p.print(255L);
p.print(std::string("ready"));
} from session import run
# hiding.cpp as it is, then with the call that passes text (-DWITH_TEXT turns on the #ifdef), then the fix.
run("""
clang++ -std=c++23 -Wall -Wextra hiding.cpp -o hiding && ./hiding
g++-16 -std=c++23 -Wall -Wextra hiding.cpp -o hiding-gcc && ./hiding-gcc
clang++ -std=c++23 -Wall -Wextra -DWITH_TEXT -c hiding.cpp
g++-16 -std=c++23 -Wall -Wextra -DWITH_TEXT -c hiding.cpp
clang++ -std=c++23 -Wall -Wextra hiding_fixed.cpp -o fixed && ./fixed
g++-16 -std=c++23 -Wall -Wextra hiding_fixed.cpp -o fixed-gcc && ./fixed-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra hiding.cpp -o hiding && ./hiding
hex 0xff
$ g++-16 -std=c++23 -Wall -Wextra hiding.cpp -o hiding-gcc && ./hiding-gcc
hex 0xff
$ clang++ -std=c++23 -Wall -Wextra -DWITH_TEXT -c hiding.cpp
hiding.cpp:20:13: error: no viable conversion from 'std::string' (aka 'basic_string<char>') to 'long'
20 | p.print(std::string("ready"));
| ^~~~~~~~~~~~~~~~~~~~
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/string:1223:55: note: candidate function
1223 | _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 operator __self_view() const _NOEXCEPT {
| ^
hiding.cpp:13:21: note: passing argument to parameter 'value' here
13 | void print(long value) const { std::println("hex {:#x}", value); }
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -DWITH_TEXT -c hiding.cpp
hiding.cpp: In function 'int main()':
hiding.cpp:20:18: error: cannot convert 'std::string' {aka 'std::__cxx11::basic_string<char>'} to 'long int'
20 | p.print(std::string("ready"));
| ^~~~~~~~~~~~~~~
| |
| std::string {aka std::__cxx11::basic_string<char>}
hiding.cpp:13:21: note: initializing argument 1 of 'void HexPrinter::print(long int) const'
13 | void print(long value) const { std::println("hex {:#x}", value); }
| ~~~~~^~~~~
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra hiding_fixed.cpp -o fixed && ./fixed
int 255
hex 0xff
text "ready"
$ g++-16 -std=c++23 -Wall -Wextra hiding_fixed.cpp -o fixed-gcc && ./fixed-gcc
int 255
hex 0xff
text "ready"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hiding_session.py
When the compiler looks up a member name, it searches the derived class first, and it stops at the first class that
declares the name. Because HexPrinter declares a print, the two functions of Printer are never considered, so
overload resolution: How the compiler chooses the function a call means: it keeps the functions of that name the arguments can be converted for, ranks each conversion (exact match, promotion, conversion) and picks the one best, or reports an ambiguous call. More in “C++ function overloading and default arguments” chooses among one candidate. This is
name hiding: What happens when a derived class declares a member with the same name as a member of its base: name lookup stops at the derived class, so every base member of that name is out of reach there, whatever its parameters.. Step through the lookup of p.print(255), then the same call with the fix:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Look up the name first. Before it compares any parameter, the compiler looks up the name print, starting in the class of p, HexPrinter.
- Lookup stops here. HexPrinter declares a print, so lookup stops in HexPrinter. Printer's two functions are never considered, whatever their parameters.
- One candidate. Overload resolution chooses among one function. 255 converts to long, so the call compiles on both compilers without a warning, even with -Wall -Wextra.
- The silent change. The program prints hex 0xff, although print(int) would have matched exactly. With -DWITH_TEXT, the call with a std::string fails: no conversion to long exists.
- The one-line fix. using Printer::print; in HexPrinter brings every print of Printer into HexPrinter's scope. Lookup still stops in HexPrinter, but now finds three functions.
- The best match wins. All three take part in overload resolution, and print(int) matches 255 exactly: the fixed program prints int 255.
- Every call finds its own. The other two calls pick their best match too: p.print(255L) prints hex 0xff and p.print(std::string("ready")) prints text "ready", on both compilers.
The session shows both consequences of the hiding:
p.print(255)compiles without any warning on either compiler, even with-Wall -Wextra, and callsprint(long): the program printshex 0xffalthoughPrinter::print(int)would have matched exactly. This silent change is the dangerous one, because nothing tells you about it.p.print(std::string("ready")), compiled when-DWITH_TEXTdefines the macro that the#ifdefchecks, fails on both compilers: there is no conversion fromstd::stringtolong. Clang’s first note points into libc++’s own string header, at a conversion it tried and rejected.
The fix is one line in the derived class, using Printer::print;. This
using-declaration: A declaration that makes a name declared elsewhere usable in this scope, as using std::swap; does. Inside a derived class, using Base::print; makes every print of the base visible there, so all of them take part in overload resolution. brings every print of Printer into the scope of HexPrinter,
so all three are candidates and each call gets its best match: int 255, hex 0xff and text "ready". Rule
C.138 of the C++ Core Guidelines recommends exactly this whenever a derived class adds an overload. If you only need
one hidden function once, a qualified call such as p.Printer::print(7) also reaches it.
Adding an overload in a derived class on its own
void print(long value) const; alone in HexPrinter hides print(int) and print(const std::string&) of
Printer, so p.print(255) silently converts 255 and prints hex 0xff. Write using Printer::print; next to it,
and each call finds its best match again.
Hiding works by name alone. It happens for any member name, with any parameter list, and for virtual functions too, where the next lesson shows a warning that catches some of the cases.
Checkpoint
Check: Name hiding and using-declarations
Private data and protected functions keep the rule in one place
Members can be public, private or protected. A protected member: A member declared after protected: in a class. The class's own member functions and those of the classes derived from it may use it, but other code may not. can be used
by the member functions of derived classes but not by other code. It is tempting to make a base class’s data protected
so that derived classes can change it directly, but compare the two designs in this program:
// Two ways to let derived classes update a percentage that must stay between 0 and 100.
#include <algorithm>
#include <print>
// 1. Protected data: each derived class has to remember the rule on its own.
class ProgressData {
public:
int percent() const { return percent_; }
protected:
int percent_ = 0; // must stay between 0 and 100
};
class DownloadA : public ProgressData {
public:
void received(long bytes, long expected) { percent_ = static_cast<int>(bytes * 100 / expected); }
};
// 2. Private data and a protected function: the base class keeps the rule in one place.
class Progress {
public:
int percent() const { return percent_; }
protected:
void set_percent(long value) { percent_ = static_cast<int>(std::clamp(value, 0L, 100L)); }
private:
int percent_ = 0;
};
class DownloadB : public Progress {
public:
void received(long bytes, long expected) { set_percent(bytes * 100 / expected); }
};
int main() {
// The server announced 2,000 bytes and then sent 2,400.
DownloadA a;
a.received(2'400, 2'000);
DownloadB b;
b.received(2'400, 2'000);
std::println("protected data: {} %", a.percent());
std::println("protected function: {} %", b.percent());
} from session import run
# progress.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra progress.cpp -o progress && ./progress
g++-16 -std=c++23 -Wall -Wextra progress.cpp -o progress-gcc && ./progress-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra progress.cpp -o progress && ./progress protected data: 120 % protected function: 100 % $ g++-16 -std=c++23 -Wall -Wextra progress.cpp -o progress-gcc && ./progress-gcc protected data: 120 % protected function: 100 %
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 progress_session.py
ProgressData documents its rule in a comment, and DownloadA, written by someone who never read the comment, stores
120 when the server sends more bytes than it announced. Progress keeps the number private and offers derived classes
a protected function, set_percent(), which clamps every value into the range, so DownloadB reports 100. Set the
two designs side by side:
| Criterion | ProgressData: protected data | Progress: a protected function |
|---|---|---|
| Who keeps 0 to 100 | Every derived class, from a comment | Progress, in set_percent() |
| The download reports | 120 % (DownloadA) | 100 % (DownloadB) |
| A class derived later | Can break the rule | Cannot write the value |
With protected data, the code that must respect the base class’s invariant: A rule about an object's data that holds whenever none of its member functions is running, such as "the balance is never negative". A class keeps it by making the data private and checking every change in its member functions. More in “C++ classes, member functions and access control” is every
derived class ever written, including those written years later by someone else. That is why rule C.133 of the C++
Core Guidelines advises against protected data. Protected functions are fine: they are an interface meant for derived
classes, and the exercise of this lesson uses one, Account::credit().
Checkpoint
Check: Private data, protected functions
Derive only when the new class is a kind of its base
Public inheritance is a promise that a derived object is a base object: a circle is a shape, and a savings account is an account. Before you derive, check the promise. If code written for the base class would be surprised by an object of the derived class, inheritance is the wrong tool, and holding the other class as a member is usually the better design. Three tests decide most cases:
- Inherit when the derived class really is a kind of the base class and can be used everywhere the base is used. A square that cannot keep a rectangle’s promise that width and height change independently is the classic case that fails this test.
- Inherit to share an interface, with or without some code. To reuse code alone, hold an object of the other class as a member; a later lesson of this module compares the two designs.
- Derive only from classes designed to be bases. The standard containers have no virtual functions, not even a virtual
destructor, so a class derived from
std::vectoris a trap; the next lesson shows why the destructor matters.
Answer the three questions for a class you want to write, and the table marks the design that fits:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Design | Works where the base does | Reuses the code | What callers see |
|---|---|---|---|
| Public inheritance Your class gets: the base inside it, and the base's public functions | Good a Circle goes to every const Shape& and Shape* without a cast | Good name() and rename() come with Shape, written once | Mixed every public function of the base, wanted or not |
| A member Your class gets: an object of the other class, used inside it | Poor not a Shape: a function that takes a Shape& refuses it | Good the member does the work; each call is forwarded on purpose | Good only the functions your class chooses to offer |
How the answers decide
- “Can every piece of code written for the base use your class without a surprise, as code for any Shape can use a Circle?” No, like a square that must keep width and height equal: rules out Public inheritance. Code written for the base would be surprised: a square that changes both sides breaks a rectangle's promise that width and height change apart.
- “Do callers need your class where the base is expected, or do you only want its code?” Only its code, inside my class: rules out Public inheritance. Reuse alone is a member's job. Deriving would also hand callers every public function of the base and let them treat your class as one.
- “Do callers need your class where the base is expected, or do you only want its code?” Callers pass it as the base: adds a caveat to A member. A member is not a base: a function that takes the base by reference refuses your class, so callers could not pass it there.
- “Was the base written to be derived from, with a virtual destructor if objects are deleted through it?” No, like std::vector, which has no virtual functions: rules out Public inheritance. Derive only from classes designed to be bases. The standard containers have no virtual destructor, so deleting your object through a pointer to one is undefined behaviour.
In short: derive publicly when your class keeps every promise of a base that was designed to be derived from, and callers need it where the base is expected; otherwise hold the other class as a member and offer only the functions your class needs.
Checkpoint
Check: When public inheritance fits
Practise: a savings account
account.h contains a finished Account and a SavingsAccount with three tasks. Amounts are whole numbers of the
smallest unit of money, so 1'000'000 is 10,000.00 and every sum is exact. The yearly interest rate is in basis
points, hundredths of a per cent, so 350 means 3.50 %.
C header · account.h
// Exercise: finish SavingsAccount so that every check in account_tests.cpp passes with both compilers.
// Account is complete; do not change it. Amounts are whole numbers of the smallest unit of money (paise or cents),
// so every sum is exact: 1'000'000 is 10,000.00.
#pragma once
#include <cstdint>
#include <string>
#include <utility>
using Money = std::int64_t;
class Account {
public:
explicit Account(std::string owner, Money opening = 0) : owner_(std::move(owner)), balance_(opening) {}
const std::string& owner() const { return owner_; }
Money balance() const { return balance_; }
// Adds a deposit. Zero or less is refused.
bool deposit(Money amount) {
if (amount <= 0) return false;
balance_ += amount;
return true;
}
// Takes money out. Refused when the amount is zero or less, or more than the balance.
bool withdraw(Money amount) {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
protected:
// For derived classes: adds money that is not a deposit, such as interest. Zero or less is ignored.
void credit(Money amount) {
if (amount > 0) balance_ += amount;
}
private:
std::string owner_;
Money balance_;
};
// A savings account earns interest once a month. The yearly rate is in basis points: 350 means 3.50 %.
class SavingsAccount : public Account {
public:
// TODO 2: give owner and opening to Account's constructor.
SavingsAccount(std::string owner, int rate_bp, Money opening = 0) : Account("?"), rate_bp_(rate_bp) {
(void)owner;
(void)opening;
}
int rate_bp() const { return rate_bp_; }
// A deposit that keeps a note for the passbook.
// TODO 1: the tests also call deposit(amount) with one argument. This declaration hides Account::deposit,
// so those calls do not compile yet.
bool deposit(Money amount, const std::string& note) {
if (!Account::deposit(amount)) return false;
last_note_ = note;
return true;
}
const std::string& last_note() const { return last_note_; }
// TODO 3: add one month of interest, balance × rate_bp ÷ 10,000 ÷ 12 rounded down to a whole unit, with
// credit(), and return the amount added.
Money add_monthly_interest() { return 0; }
private:
int rate_bp_;
std::string last_note_;
}; C++ · account_tests.cpp
// Sample tests for account.h. Each check prints what it expects when it fails; the exit status is 0 only when
// every check passes.
#include <print>
#include "account.h"
namespace {
int checks = 0;
int passed = 0;
void check(bool ok, const char* what) {
++checks;
if (ok) ++passed;
else std::println("FAILED: {}", what);
}
} // namespace
int main() {
SavingsAccount s{"Meera", 350, 1'000'000}; // 10,000.00 at 3.50 % a year
check(s.owner() == "Meera", "the owner reaches Account");
check(s.balance() == 1'000'000, "the opening balance reaches Account");
check(s.deposit(50'000), "deposit(amount) works on a SavingsAccount");
check(s.deposit(25'000, "salary") && s.last_note() == "salary", "deposit(amount, note) keeps the note");
check(s.balance() == 1'075'000, "both deposits are in the balance");
Account& plain = s; // a SavingsAccount is an Account
check(plain.withdraw(75'000) && s.balance() == 1'000'000, "a SavingsAccount works through an Account&");
check(s.add_monthly_interest() == 2'916, "one month on 10,000.00 at 3.50 % adds 29.16");
check(s.balance() == 1'002'916, "the interest is in the balance");
check(s.add_monthly_interest() == 2'925, "the second month also earns on the first month's interest");
std::println("{} of {} checks passed", passed, checks);
return passed == checks ? 0 : 1;
} Build the tests before you change anything. They do not even compile yet:
from session import run
# The sample tests built against the starter code with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra account_tests.cpp -o account_tests && ./account_tests
g++-16 -std=c++23 -Wall -Wextra account_tests.cpp -o account_tests-gcc && ./account_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra account_tests.cpp -o account_tests && ./account_tests
account_tests.cpp:22:13: error: too few arguments to function call, expected 2, have 1; did you mean 'Account::deposit'?
22 | check(s.deposit(50'000), "deposit(amount) works on a SavingsAccount");
| ^~~~~~~
| Account::deposit
./account.h:18:10: note: 'Account::deposit' declared here
18 | bool deposit(Money amount) {
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra account_tests.cpp -o account_tests-gcc && ./account_tests-gcc
account_tests.cpp: In function 'int main()':
account_tests.cpp:22:20: error: no matching function for call to 'SavingsAccount::deposit(int)'
22 | check(s.deposit(50'000), "deposit(amount) works on a SavingsAccount");
| ~~~~~~~~~^~~~~~~~
• there is 1 candidate
In file included from account_tests.cpp:5:
• candidate 1: 'bool SavingsAccount::deposit(Money, const std::string&)'
account.h:56:10:
56 | bool deposit(Money amount, const std::string& note) {
| ^~~~~~~
• candidate expects 2 arguments, 1 provided
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 account_session.py
- TODO 1: the tests call
depositwith one argument, but the two-argumentdepositofSavingsAccounthidesAccount::deposit. Read Clang’s hint, then make both calls work without writing a new one-argument function. - TODO 2: pass
ownerandopeningon toAccount’s constructor in the member initialiser list. - TODO 3: write
add_monthly_interest(). One month of interest is balance × rate ÷ 10,000 ÷ 12, rounded down to a whole unit: 10,000.00 at 3.50 % earns 29.16 in the first month. Add it with the protected functioncredit(), because the balance is private toAccount.
You are done when the tests report 9 of 9 checks passed with Clang and with GCC. Then add a std::println to the
constructors and destructors of both classes and predict the order of the lines before you run the tests again.
Then try the six ideas of this lesson, from the base class subobject to the is-a test, on cases it has not shown. The session starts with whichever idea your answers make weakest, and each choice you pick says what it gets right or misses:
Practice that adapts to you
Practise this lesson
15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · In what order are the parts of a derived object constructed and destroyed?
Construction goes from the base to the most derived class. The base class subobjects are constructed first, in the order they are listed after the colon; then the derived class’s members, in the order they are declared; then the derived constructor’s body runs. Each base follows the same rule inside, so its members come before its own body. Destruction is the exact reverse: the derived destructor’s body, then the derived members in reverse order of declaration, then the base parts. The order of the member initialiser list changes nothing, which is why compilers warn with -Wreorder when the list is written differently. A good answer adds the consequence: while the base constructor runs, the derived members do not exist yet, so the base must never depend on them.
An interviewer often tests the same rule on a case the answer did not cover, a class with two bases or a destructor that uses its base. Decide both:
Checkpoint
Interview round: two follow-up questions
Two harder questions of this module start from this lesson: a call that changed its meaning when a derived class added one overload, and a balance that went negative through protected data:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A derived class adds one overload, print(long). A call p.print(255) that used to reach the base class's print(int) now prints a hex number, and neither compiler warned. Explain the rule behind it, and compare the ways to fix it.
-
Mid to senior
A base class Account has a protected balance_ that four derived classes update directly, and production shows a negative balance. How would you change the design so that Account can guarantee its rule, and what does the change cost?
Key takeaways
class D : public Bgives everyDobject a completeBsubobject plusD’s own members, and aDconverts to aB&or aB*without a cast.- The derived constructor builds its base part by naming a base constructor in its member initialiser list, and must
when the base has no default constructor; a base is private by default for a
class, so writepublic. - Construction runs base parts, then members in declaration order, then the body; destruction runs it backwards, and the initialiser list’s order changes nothing.
- One member function in a derived class hides every base member with the same name, silently when a conversion makes
the call still compile, and
using Base::name;brings the base overloads back. - Keep base-class data private, and give derived classes protected functions that enforce the base class’s rules.
- Derive publicly only when the new class keeps every promise of a base designed to be derived from; to reuse code alone, hold the other class as a member.
Terms in this lesson
- base class
- The class another class derives from: in class Circle : public Shape { … };, Shape is the base class. Every object of the derived class contains a complete object of the base, its base class subobject.
- base class subobject
- The complete base class object inside every object of a derived class. It is built before the derived class's own members and destroyed after them, and a reference or a pointer to the base refers to it.
- derived class
- A class declared with a base class after a colon, as in class Circle : public Shape. It has every member of its base plus its own, and with public inheritance it can be used wherever a reference or a pointer to the base is expected.
- name hiding
- What happens when a derived class declares a member with the same name as a member of its base: name lookup stops at the derived class, so every base member of that name is out of reach there, whatever its parameters.
- protected member
- A member declared after protected: in a class. The class's own member functions and those of the classes derived from it may use it, but other code may not.
- using-declaration
- A declaration that makes a name declared elsewhere usable in this scope, as using std::swap; does. Inside a derived class, using Base::print; makes every print of the base visible there, so all of them take part in overload resolution.
References
- Working Draft, Programming Languages: C++ (N4950): [class.derived], [class.access.base], [class.protected], [class.base.init], [class.dtor], [class.member.lookup], [namespace.udecl] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: C.133, avoid protected data (Standard C++ Foundation)
- C++ Core Guidelines: C.138, create an overload set for a derived class and its bases with using (Standard C++ Foundation)
- C++ Core Guidelines: C.120, use class hierarchies to represent concepts with inherent hierarchical structure (Standard C++ Foundation)
- C++ FAQ: Inheritance, what does "Derived::f(char) hides Base::f(double)" mean? (Standard C++ Foundation)
- Options Controlling C++ Dialect (Using the GNU Compiler Collection) (GNU Project)
- Diagnostic flags in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress