Ch16.1: What Inheritance Is: the Base Subobject
1. A Derived Object Contains a Base Object
struct labeled_point : point does not copy point's code into
labeled_point. It makes every labeled_point
contain a point, as a part of itself, and calls that part
the base subobject. Everything else about inheritance follows from
that one sentence.
struct point
{
::std::int_least32_t x{}, y{};
[[nodiscard]] ::std::int_least32_t sum() const noexcept { return x + y; }
};
struct labeled_point : point
{
::std::string_view label{};
};
p.xandp.ywork on alabeled_point: they are members of its base subobject, and name lookup finds them.p.sum()works:sumwas declared inpointand is inherited. Nothing was generated for it inlabeled_point.sizeof(labeled_point)is at leastsizeof(point)plus the new members — here24, on a machine wherepointis8and a::std::string_viewis16.- A
labeled_pointis apoint: apoint const&can be bound to it (Ch16.5).
The relation is written from the derived side, and the access specifier in front of
the base name is not decoration — it is public, protected
or private, and it changes what the rest of the program may do with the
relationship (Ch16.3).
2. The is-a Test
Public inheritance is the strongest statement about a type that C++ has: it says the derived type is a base type, for every purpose, forever. Hold the sentence and check it:
| Candidate | “A D is a B” |
Verdict |
|---|---|---|
labeled_point : point |
a labeled point is a point | yes — the label is extra, everything a point guarantees still holds |
point : labeled_point |
a point is a labeled point | no — a plain point has no label |
car : engine |
a car is an engine | no — a car has an engine (Ch16.8) |
square : rectangle |
a square is a rectangle | tempting and wrong — a rectangle can be resized to 2×3, a square
cannot, so a square& passed as a
rectangle& would be breakable |
The last row is the classic reason to be strict. Public inheritance means the base class's promises hold for the derived object for all callers, including ones written before the derived class existed. “Is-a” is not about the real world; it is about substitutability.
3. Layout: the Base Comes First
A base subobject sits at the start of a derived object with no
virtual inheritance involved, which is why a base reference to a derived
object needs no adjustment:
| Expression | Type | Value |
|---|---|---|
sizeof(point) |
::std::size_t |
8 |
sizeof(labeled_point) |
::std::size_t |
24 |
sizeof(std::string_view) |
::std::size_t |
16 |
No padding is added for the inheritance itself: a derived class with no new data members is exactly as large as its base. That is the empty base optimisation's business and this chapter's last-but-one surprise (Ch16.8).
4. The Whole Program
#include <fast_io.h>
#include <cstdint>
#include <string_view>
struct point
{
::std::int_least32_t x{}, y{};
[[nodiscard]] ::std::int_least32_t sum() const noexcept { return x + y; }
};
struct labeled_point : point
{
::std::string_view label{};
};
int main()
{
try
{
labeled_point p{};
p.x = 3;
p.y = 4;
p.label = "corner";
::fast_io::io::println(p.label, " ", p.sum());
::fast_io::io::println("sizeof(point)=", sizeof(point), " sizeof(labeled_point)=", sizeof(labeled_point));
point const &base{p};
::fast_io::io::println("base.sum()=", base.sum());
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
corner 7
sizeof(point)=8 sizeof(labeled_point)=24
base.sum()=7
The third line is the interesting one: base is a reference to the
point subobject inside p, not to a copy, and it was created
without a cast.
5. Rules of Thumb
- Say the is-a sentence out loud before writing the colon. If it is not literally true for every caller, use a member instead.
- Write
: public baseexplicitly; on astructthepublicis implied, and on aclassthe opposite is, so the word prevents a silent change of meaning when someone editsstructtoclass. - Keep base classes small and self-contained. Everything you put in a base class is inherited by everything below it, including invariants that future code must maintain.
- Do not use inheritance to reuse code. A base class is a type, not a macro; reuse is what functions and templates are for.
- Do not inherit from a type you do not own unless its author documented that it is a base class (a public, non-virtual destructor being the usual sign the opposite is intended).