Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

shapes.cpp C++ · shapes.cpp
// 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));
}
Built and run with both compilers Python · shapes_session.py
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 Circle object holds a Shape sub-object, built first, then its own radius_; a const Shape& refers to the Shape part.const Shape& shapein print_name()wheel, a Circle object32 bytes with libc++40 bytes with libstdc++Shape sub-objectname_ = "front wheel"built 1st, destroyed lastCircle's own memberradius_ = 0.35built 2nd, destroyed firstthenrefers to theShape partsizeof(Circle)

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Question 1 of 2 print_name(wheel) passes the Circle called wheel to a function that takes const Shape& shape. What does shape refer to inside the function?

    Choose one answer.

    A hint

    Follow the arrow of print_name() in the drawing of wheel.

    Show the answer: Question 1 of 2

    Answer: The Shape part inside wheel; nothing is copied

    With public inheritance a Circle converts to a const Shape& implicitly, and the reference binds to the Shape subobject inside wheel, where the arrow of print_name() ends in the drawing. Nothing is copied: the reference is another name for that part of wheel.

  2. Question 2 of 2 With GCC 16's library, libstdc++, sizeof(Shape) printed 32, the size of its one std::string. Circle adds a double of 8 bytes. What did sizeof(Circle) print with GCC?

    Type a number.

    A hint

    A Circle holds a whole Shape, then its own member.

    Show the answer: Question 2 of 2

    Answer: 40 bytes

    Every Circle contains a complete Shape, 32 bytes with libstdc++, followed by radius_, 8 bytes: 40 in all, as the GCC run printed. With Apple clang's libc++ a std::string is 24 bytes, so the same Circle is 32.

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:

A missing base constructor, and a missing public

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);
}
What each compiler reports Python · base_errors_session.py
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:

The access of a base class, by the keyword that declares the derived class
Criterion Declared with classDeclared with struct
No word before the base A private baseA public base
show_balance(s) from main Error: an inaccessible baseCompiles
public before the base A public baseA 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

  1. Question 1 of 2 Account has only the constructor explicit Account(std::string owner), and SavingsAccount derives from it publicly and has a member int rate_bp_. Which constructors of SavingsAccount compile?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • SavingsAccount(std::string owner, int rate_bp) : Account(std::move(owner)), rate_bp_(rate_bp) {}
    • SavingsAccount(std::string owner, int rate_bp) : rate_bp_(rate_bp), Account(std::move(owner)) {}

    A derived constructor builds its base part by naming a base constructor in its member initialiser list. Without a default constructor in Account it must name one. Where it appears in the list does not matter: the base is always built first, which is why the second constructor compiles with only a warning.

  2. Question 2 of 2 struct SavingsAccount : Account {}; has no word before Account. Can main pass a SavingsAccount to show_balance(const Account&)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because a class declared with struct inherits publicly by default

    With no access specifier, a base is public when the derived class is declared with struct and private when it is declared with class. Writing public says what you mean with either keyword.

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:

order.cpp C++ · order.cpp
// 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");
}
Warnings first, then the order of every step Python · order_session.py
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:

Interactive · step by step Every step of Circle c{2.5}, from the first member to the last Step through the run of order.cpp. The left column is the Shape part of c, the right one what Circle adds; each box says when it was built and destroyed.
Every step of Circle c{2.5}, from the first member to the last, step 9 of 9. Showing Shape::label_ (built 1st, destroyed 4th), Shape() body (ran 2nd; ~Shape() 3rd), Circle::centre_ (built 3rd, destroyed 2nd), Circle(2.5) body (ran 4th; ~Circle() 1st). In focus: Shape::label_.Shape::label_built 1st, destroyed 4thShape() bodyran 2nd; ~Shape() 3rdCircle::centre_built 3rd, destroyed 2ndCircle(2.5) bodyran 4th; ~Circle() 1st

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. 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.
  2. 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_.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Circle's members go. Then Circle's members are destroyed: destroy member Circle::centre_.
  8. Then the base part. The base part goes after everything Circle added, and inside it the body runs first: Shape destructor body.
  9. 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

  1. Question 1 of 2 Circle's member initialiser list names centre_ before Shape(). Which line does order.cpp print first after creating a Circle:?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: construct member Shape::label_

    The order comes from the class definition: the base class first (and inside Shape, its members before its body), then Circle's members in declaration order, then Circle's body. Both compilers warned that the list did not follow it.

  2. Question 2 of 2 When c goes out of scope at the closing brace, which of its parts is destroyed last?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Shape::label_, the part built first

    The run printed Circle destructor body, destroy member Circle::centre_, Shape destructor body and destroy member Shape::label_: construction in reverse.

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:

The bug, and the fix

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"));
}
Three builds of the same idea Python · hiding_session.py
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:

Interactive · step by step How the compiler finds print for p.print(255) Step through the lookup of the name print, first in hiding.cpp, then with the one-line fix. The left column is HexPrinter, the right one its base, Printer.
How the compiler finds print for p.print(255), step 7 of 7. Showing p.print(255) (prints int 255), print(long), using Printer::print;, print(int) (visible in HexPrinter), print(const string&) (visible in HexPrinter). In focus: print(long), print(const string&).exact matchp.print(255)prints int 255print(long)declared in HexPrinterusing Printer::print;the fixprint(int)visible in HexPrinterprint(const string&)visible in HexPrinter

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Look up the name first. Before it compares any parameter, the compiler looks up the name print, starting in the class of p, HexPrinter.
  2. Lookup stops here. HexPrinter declares a print, so lookup stops in HexPrinter. Printer's two functions are never considered, whatever their parameters.
  3. 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.
  4. 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.
  5. 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.
  6. The best match wins. All three take part in overload resolution, and print(int) matches 255 exactly: the fixed program prints int 255.
  7. 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 calls print(long): the program prints hex 0xff although Printer::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_TEXT defines the macro that the #ifdef checks, fails on both compilers: there is no conversion from std::string to long. 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

  1. Question 1 of 2 HexPrinter declares print(long) and derives from Printer, which declares print(int) and print(const std::string&). How many functions does overload resolution consider for p.print(255)?

    Type a number.

    A hint

    Name lookup runs before any parameter is compared. Where does it stop?

    Show the answer: Question 1 of 2

    Answer: 1 functions

    Name lookup stops in HexPrinter, the first class that declares print, so Printer's two functions never become candidates. One function is left, and 255 converts to long.

  2. Question 2 of 2 With using Printer::print; added to HexPrinter, what does this print?

    Read the code, then choose one answer.

    HexPrinter p;
    p.print(255);
    p.print(255L);
    Show the answer: Question 2 of 2

    Answer: int 255, then hex 0xff

    The using-declaration brings Printer's functions into HexPrinter's scope, and overload resolution chooses among all three: hiding_fixed.cpp printed int 255 and hex 0xff on both compilers.

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:

progress.cpp C++ · progress.cpp
// 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());
}
The same download, two base classes Python · progress_session.py
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:

The same 2,400 bytes of an announced 2,000, through two base classes
Criterion ProgressData: protected dataProgress: a protected function
Who keeps 0 to 100 Every derived class, from a commentProgress, in set_percent()
The download reports 120 % (DownloadA)100 % (DownloadB)
A class derived later Can break the ruleCannot 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

  1. Question 1 of 2 The server announced 2,000 bytes and sent 2,400. What percentage does DownloadA, which writes the protected percent_ directly, report?

    Type a number.

    A hint

    Work out bytes × 100 ÷ expected, and ask what stops a value above 100.

    Show the answer: Question 1 of 2

    Answer: 120 %

    DownloadA stores 2,400 × 100 ÷ 2,000 = 120, because nothing stops it: the rule lives in a comment. DownloadB goes through set_percent(), which clamps every value, and reports 100.

  2. Question 2 of 2 With Progress, which code has to keep percent_ between 0 and 100?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only Progress, in set_percent(), because percent_ is private

    Private data and a protected function put the rule in one place that the base class controls. Derived classes call set_percent() like an interface, and a class derived next year cannot store 120.

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::vector is 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:

Interactive · answer and compare Derive from the class, or hold it as a member? Answer three questions about the class you want to write and the class you want to build on. The table marks the design that fits and says why the other does not.
Can every piece of code written for the base use your class without a surprise, as code for any Shape can use a Circle?
Do callers need your class where the base is expected, or do you only want its code?
Was the base written to be derived from, with a virtual destructor if objects are deleted through it?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Public inheritance and a member, against the three tests of this section
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

  1. One question A teammate writes class Scores : public std::vector<int> {}; to get push_back and size without writing them, and the program deletes Scores objects through a std::vector<int>*. What is wrong?

    Choose one answer.

    Show the answer: One question

    Answer: std::vector has no virtual destructor, so that delete is undefined behaviour; hold the vector as a member instead

    Deleting an object through a pointer to a base class whose destructor is not virtual is undefined behaviour, and std::vector's is not. Hold the vector as a member and offer the functions Scores needs.

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 %.

The starter code and its sample tests

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:

The sample tests against the starter code Python · account_session.py
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

  1. TODO 1: the tests call deposit with one argument, but the two-argument deposit of SavingsAccount hides Account::deposit. Read Clang’s hint, then make both calls work without writing a new one-argument function.
  2. TODO 2: pass owner and opening on to Account’s constructor in the member initialiser list.
  3. 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 function credit(), because the balance is private to Account.

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.

  1. Question 1 of 15 Circle declares only a constructor, radius() and area(). Why does wheel.name() compile?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Circle inherits Shape's public members, and with public inheritance they stay public

    A derived class inherits the members of its base. With public inheritance, Shape's public name() and rename() are public members of Circle too, so wheel.name() works as if Circle had declared it.

  2. Question 2 of 15 After wheel.rename("front wheel");, what does this print?

    Read the code, then choose one answer.

    Shape* any = &wheel;
    std::println("through a Shape*: {}", any->name());
    Show the answer: Question 2 of 15

    Answer: through a Shape*: front wheel

    A pointer to the base points at the base class subobject inside the derived object. shapes.cpp printed through a Shape*: front wheel with both compilers.

  3. Question 3 of 15 class SavingsAccount : Account { … }; has no access specifier before Account. What kind of inheritance is it?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Private, because the derived class is declared with the keyword class

    A base class is private by default when the derived class is declared with class, and public when it is declared with struct. Outside SavingsAccount, a SavingsAccount then cannot be used as an Account, as both compilers report.

  4. Question 4 of 15 Account has only the constructor explicit Account(std::string owner). What happens with this derived class?

    Read the code, then choose one answer.

    class SavingsAccount : public Account {
    public:
        explicit SavingsAccount(int rate_bp) : rate_bp_(rate_bp) {}
    private:
        int rate_bp_;
    };
    Show the answer: Question 4 of 15

    Answer: The constructor does not compile, because nothing says which Account constructor builds the base part

    A base part is always constructed before the derived members. Without a default constructor in Account, the member initialiser list has to name one, for example SavingsAccount(std::string owner, int rate_bp) : Account(std::move(owner)), rate_bp_(rate_bp) {}.

  5. Question 5 of 15 Circle derives from Shape, and both classes have members. Put the steps of Circle c{2.5}; in the order they run.

    Give each item its position, from 1 (first).

    Show the answer: Question 5 of 15

    Answer:

    1. Shape's members are constructed
    2. Shape's constructor body runs
    3. Circle's members are constructed
    4. Circle's constructor body runs

    The base part comes first, members before the body, at every level. The order of the member initialiser list changes nothing, which is why -Wall warns when the list is written in another order. Destruction runs the same steps backwards.

  6. Question 6 of 15 Put the steps of destroying that Circle c in the order they run when its block ends.

    Give each item its position, from 1 (first).

    Show the answer: Question 6 of 15

    Answer:

    1. Circle's destructor body runs
    2. Circle's members are destroyed
    3. Shape's destructor body runs
    4. Shape's members are destroyed

    Destruction is construction backwards: the most derived class's body first, then its members, then the base part, whose own body runs before its members go. order.cpp printed exactly this.

  7. Question 7 of 15 Circle gets a member std::string label_ and the initialiser label_(name() + " (circle)"), which calls the inherited name(). Is that safe?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Yes, because the base part is complete before any member of Circle is initialised

    A derived class's member initialisers may use the base part, which is already complete. The reverse is never safe: the base constructor runs before any derived member exists.

  8. Question 8 of 15 What does using Printer::print; do inside HexPrinter?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: It makes every print of Printer visible in HexPrinter, so all of them take part in overload resolution

    Lookup still stops in HexPrinter, but with the using-declaration it finds all three print functions there, so the best match wins. Rule C.138 of the C++ Core Guidelines recommends it whenever a derived class adds an overload.

  9. Question 9 of 15 What does this program print, built with either compiler?

    Read the code, then choose one answer.

    struct Printer {
        void print(int v) const { std::println("int {}", v); }
    };
    struct HexPrinter : Printer {
        void print(long v) const { std::println("hex {:#x}", v); }
    };
    int main() { HexPrinter p; p.print(255); }
    Show the answer: Question 9 of 15

    Answer: hex 0xff

    HexPrinter declares a print of its own, so name lookup stops in HexPrinter and never sees Printer::print(int). 255 converts to long, and HexPrinter::print(long) runs without any warning. using Printer::print; inside HexPrinter would bring the int overload back, and then it would win.

  10. Question 10 of 15 Suppose HexPrinter declared no function called print at all, and named its own hex(long). What would this print?

    Read the code, then choose one answer.

    HexPrinter p;
    p.print(std::string("ready"));
    p.print(255);
    Show the answer: Question 10 of 15

    Answer: text "ready", then int 255

    Hiding needs a declaration of the same name in the derived class. Without one, lookup goes on to Printer, finds both of its print functions, and each call gets its exact match: text "ready", then int 255.

  11. Question 11 of 15 Account::credit() is a protected member function. Which code may call it?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Account's own member functions and those of classes derived from it, such as SavingsAccount

    A protected member can be used by the class's own member functions and by those of its derived classes, but not by other code. That is why the exercise adds interest with credit() and the tests never call it.

  12. Question 12 of 15 A base class must guarantee that a percentage stays between 0 and 100, and derived classes need to update it. Which design lets the base class keep that promise?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: A private data member, with a protected function that checks the value before storing it

    Only code the base class controls can enforce its invariant. With protected data, every derived class, including ones written later, has to repeat the check, which is why the C++ Core Guidelines advise against protected data (rule C.133).

  13. Question 13 of 15 A class derived from Progress calls set_percent(-20) after a counting mistake. What does percent() return afterwards?

    Type a number.

    A hint

    set_percent() passes its value through std::clamp(value, 0L, 100L).

    Show the answer: Question 13 of 15

    Answer: 0 %

    std::clamp keeps a value inside the range, so −20 becomes 0, just as the 120 of 2,400 bytes out of 2,000 became 100. Because percent_ is private, no derived class can store a value outside the range by any other route.

  14. Question 14 of 15 Why does a Square derived publicly from Rectangle fail the test of public inheritance?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Code written for a Rectangle may change the width and expect the height to stay, and a Square cannot keep that promise

    Public inheritance means that a derived object is a base object for every piece of code written for the base. A Rectangle promises that its width and height change independently, which a Square cannot keep.

  15. Question 15 of 15 A Stack class needs push, pop and top, and nothing else, on top of a std::vector<int>. Which design fits?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Hold the vector as a private member and write the three functions

    This is reuse alone, which is a member's job: Stack keeps its own rule, and nothing outside can treat it as a vector. Public inheritance would hand callers every public function of std::vector.

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

  1. Question 1 of 2 struct Copier : public Scanner, public Printer has the constructor Copier() : Printer(), Scanner() {}. Which base is constructed first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Scanner, because it is listed first after the colon

    The declaration of the class decides: bases in the order they are listed after the colon, then members in declaration order, then the body. Destruction runs in reverse: ~Copier, then ~Printer, then ~Scanner.

  2. Question 2 of 2 May the body of ~Circle() call the inherited name() of Shape?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because the base part is destroyed only after the derived destructor body and members

    The base part outlives the derived destructor's body and the derived members. The opposite does not hold: in Shape's destructor, Circle's members are already gone.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • class D : public B gives every D object a complete B subobject plus D’s own members, and a D converts to a B& or a B* 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 write public.
  • 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.