Ch16.3: public, protected and private Inheritance
1. Three Kinds of Inheritance, Three Jobs
The word before the base class name is an access specifier, and it answers two questions at once: what may the derived class do with the base's members, and what may the rest of the program do with the derived-to-base relationship?
| Written as | Means | Outside code may | Use it for |
|---|---|---|---|
: public base |
is-a | convert D& to B&, use
public base members |
the only kind that states a type relationship |
: private base |
implemented in terms of | nothing at all | adopting an existing implementation and publishing exactly
the operations you choose with using — §5, and this is
one of the most useful idioms in the language |
: protected base |
implemented in terms of | nothing: the relationship is visible only inside the hierarchy | a further-derived class must see the base's
protected members — the rarest of the three |
Members of the base keep their own access inside the derived class: base
public members are usable by the derived class and by outside code (under
public inheritance); base protected members are usable by the derived class
and by further derived classes; base private members are usable by nobody
but the base itself.
2. protected Is a Promise to Derived Classes
class machine_part
{
protected:
::std::int_least32_t serial_{};
public:
::std::int_least32_t serial() const noexcept { return serial_; }
};
class machine : public machine_part
{
::std::int_least32_t power_{};
public:
void set_serial(::std::int_least32_t s) noexcept
{
serial_ = s; // legal: protected member, derived class
}
::std::int_least32_t power() const noexcept { return power_; }
};
serial_ is protected, so machine may write it
but the rest of the world may only call serial(). That is a real choice
with a real cost: every derived class is now part of the base's invariant. If
machine_part later wants serial_ to be validated on every
write, it cannot, because it handed the write permission to an unbounded number of
future classes.
This book's rule, stated in Ch13.4, still
holds: prefer private data, and reach for protected only when
a derived class genuinely must maintain the invariant itself — a serialisation
base, an iterator base, a stream base. A protected data member is
public data with a smaller audience.
3. What Each Kind of Inheritance Allows, Measured
The differences are not a matter of opinion; they are visible in the type system, and
::std::is_convertible_v prints them
(Ch15.1):
class base
{
protected:
::std::int_least32_t prot_{7};
public:
::std::int_least32_t pub_{1};
};
struct public_derived : public base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
struct protected_derived : protected base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
struct private_derived : private base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
All three can read prot_ and pub_ and produce
8. Only the first is a base as far as the outside world is
concerned. That is the whole difference, and it is exactly what makes private
inheritance useful: everything of the base is available to you, and
nothing of it is visible to your users until you say so.
4. using Chooses What to Publish
A using-declaration names a base member and re-publishes it at the access
level where the declaration appears:
class buffer
{
protected:
char *data_{};
::std::size_t size_{};
public:
[[nodiscard]] ::std::size_t size() const noexcept { return size_; }
};
class message : private buffer
{
public:
using buffer::size; // size() is public again
};
using buffer::size; changes the access of that one name inside
message; the inheritance itself stays private, so a
message& still does not convert to a buffer&. The
next section uses this systematically.
5. Private Inheritance Is Very Useful: Selective Exposure
Consider building a stack on top of a vector. Composition means writing a forwarding
function for every operation you want, and getting each signature, each overload and
each noexcept/throws contract right by hand. Private
inheritance plus using means writing nothing: you adopt
the whole implementation and then publish a chosen subset of it, with the original
signatures intact.
// adopt a vector's storage, publish only a stack's operations
class int_stack : private ::fast_io::vector<int>
{
using base = ::fast_io::vector<int>;
public:
using base::is_empty;
using base::push_back;
using base::size;
[[nodiscard]] int top() const throws { return base::back(); }
void pop() noexcept { base::pop_back(); }
};
size=3 is_empty=0 top=3
after pop: size=2 top=2
What the three using lines bought, and what they did not:
- No boilerplate.
push_backhas two overloads (const lvalue and rvalue) and its own error contract;usingre-publishes both exactly, forever in sync with the base. A hand-written forwarder would have collapsed them into one signature and eventually lost the contract. - Nothing else leaks.
int_stackhas nooperator[], noinsert, nodata(), no iterators, nopush_back_unchecked— anint_stackis not a::fast_io::vector<int>for any caller, and it cannot be passed to anything expecting one. The chain of substitutability is cut, deliberately. - Zero size cost. The base subobject is the whole object, so
sizeof(int_stack) == sizeof(::fast_io::vector<int>). A member would be the same, but you would have to name it and forward to it. - Still an implementation, not a type. Because the relation is
private, the base's non-virtual destructor is harmless: nobody can delete an
int_stackthrough a::fast_io::vector<int>*(Ch16.2).
Compare with the alternative from
Chapter 15: a concept plus a template is
even more flexible, but it makes int_stack a template, and sometimes you
want one concrete type. Private inheritance is the tool for “one concrete type
that is a strict subset of an existing one”. The standard library itself uses it:
::std::stack in several implementations is a private adapter over
::std::deque, and the pre-C++11
::std::istream-based adapters were built the same way.
Where it is the wrong tool: when the base is not a good implementation of your abstraction, when you want to expose most of the base anyway (then it is public inheritance or a member), and when you need to swap the implementation at run time (then it is a variant or an erased type).
6. Testing: #define private public
Access specifiers are a compile-time, header-level property, which means a test translation unit can change them before including the header it is testing:
// counter.hpp
#pragma once
#include <cstddef>
class counter
{
public:
counter() noexcept = default;
void bump() noexcept { ++hidden_; }
[[nodiscard]] ::std::size_t value() const noexcept { return hidden_; }
private:
::std::size_t hidden_{};
};
#include <fast_io.h>
#define private public
#include "counter.hpp"
#undef private
int main()
{
try
{
counter c{};
c.bump();
c.hidden_ = 41; // reachable only because of the #define
::fast_io::io::println("value()=", c.value(), " hidden_=", c.hidden_);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
value()=41 hidden_=41
Without the two preprocessor lines the same statement is rejected, which is what makes the trick worth knowing:
error: ‘std::size_t counter::hidden_’ is private within this context
It is very useful for testing — checking an invariant directly, setting up a state that the public interface cannot reach, asserting that a moved-from object really is empty — and it is a sharp tool. The rules that keep it from becoming a disaster:
- Only in a test source file. Immediately before the
#include, immediately#undefafter it. Never in a header, never in anything that ships. - It rewrites the token
privateonly. A member declared before the first access specifier is still private, because it is in theclass's implicit private section; the macro cannot reach it. (The heavier trick#define class structchanges that, and changes much more — do not use it.) - It is formally an ODR violation: the class has a different definition in the test translation unit than in the library's. Access specifiers do not affect layout, so nothing breaks in practice, and the moment the library under test is a prebuilt binary with an ABI you are not changing, it stays harmless.
- It couples the test to the implementation. Every
hidden_a test reads is a name the author may rename tomorrow. Test the public behaviour; reach inside only for invariants the public interface cannot observe. - Use the
#define/#undefpair, not-Dprivate=public. The command-line form applies to every header you include, including the standard library's, which is asking for trouble.
7. The Whole Program
#include <fast_io.h>
#include <cstdint>
#include <type_traits>
class base
{
protected:
::std::int_least32_t prot_{7};
public:
::std::int_least32_t pub_{1};
};
struct public_derived : public base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
struct protected_derived : protected base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
struct private_derived : private base
{
[[nodiscard]] ::std::int_least32_t read() const noexcept { return prot_ + pub_; }
};
int main()
{
try
{
public_derived a{};
protected_derived b{};
private_derived c{};
::fast_io::io::println("reads: ", a.read(), " ", b.read(), " ", c.read());
::fast_io::io::println("public -> base convertible: ", ::std::is_convertible_v<public_derived &, base &>);
::fast_io::io::println("protected -> base convertible: ", ::std::is_convertible_v<protected_derived &, base &>);
::fast_io::io::println("private -> base convertible: ", ::std::is_convertible_v<private_derived &, base &>);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
reads: 8 8 8
public -> base convertible: 1
protected -> base convertible: 0
private -> base convertible: 0
Three classes, one behaviour, and only one of them is a base. The two
zeros are not a restriction to work around — they are the point: the
implementation is adopted, the relationship is not published.
8. Rules of Thumb
- Use
publicinheritance when the is-a sentence is true; useprivateinheritance when you want to adopt an implementation and publish a chosen subset of it withusing. - Prefer
privatedata withprotectedaccessor functions overprotecteddata members. - Do not mix the access specifier of a base with its members' accessibilities in your
head: they multiply.
privateinheritance still lets the derived class use basepublicmembers. - With private inheritance, list the published operations in one block of
usingdeclarations, so the public interface is readable at a glance. - Remember that private inheritance is an implementation detail: no conversion, no
owning base pointers, no
virtualinterface. That is the point, not a limitation. #define private publicbelongs in test sources only, as a#define/#undefpair around one include.