AlgoMaster Logo

Access Specifiers in Inheritance

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

When you write class Discounted : public Product, the public keyword sitting between the colon and the base name is doing real work. It controls how the base class's members appear inside the derived class and to anyone using the derived class from outside. This lesson covers the three inheritance modes (public, protected, private), how each one rewrites the access of inherited members, when each mode applies, and the common mistakes that come from picking the wrong one.

Two Different Access Settings, Don't Confuse Them

C++ has access modifiers in two separate places, and they answer two different questions. Mixing them up is a common source of confusion in this topic, so it is worth pinning down before any code shows up.

The first place is inside a class body, where labels like public: and private: decide who can see each member. For example, Product declares name as public, price as private, stock as protected, and that determines who is allowed to touch each one.

The second place is between the colon and the base class name, where the keyword decides how those base members are exposed when a derived class inherits them. That second keyword is what this chapter is about, and it has its own name: the inheritance mode or inheritance access specifier.

Both spellings use the same three keywords, public, protected, private, but they are answering different questions. The label inside the class body asks "who outside this class can touch this member?". The keyword in front of the base class name asks "what access category should the base's members fall into when seen through the derived class?". Once you start reading them as two separate switches, the rest of this chapter falls into place.

The Three Inheritance Modes

Every time a derived class names a base, you pick one of three modes. The mode rewrites the access category of the inherited members as seen through the derived class. The base's own definitions do not change. Only the view from the outside does.

The rule in one sentence: an inherited member's effective access becomes the more restrictive of (its original access in the base) and (the inheritance mode). "More restrictive" means private is the strictest, protected is in the middle, and public is the loosest.

The full table:

Base member accesspublic inheritanceprotected inheritanceprivate inheritance
publicpublic in derivedprotected in derivedprivate in derived
protectedprotected in derivedprotected in derivedprivate in derived
privatenot accessiblenot accessiblenot accessible

Three rows, three columns, nine cells. Read across each row, the access can only stay the same or get tighter, never looser. Read down each column, the inheritance mode is a ceiling: nothing inherited is ever more accessible than the mode itself. The bottom row is the same in all three modes because a base's private members are off-limits to derived classes regardless. They are inherited (they exist as part of the object's storage), but the derived class cannot see their names.

The diagram below shows the same idea visually. Each base member is on the left, each inheritance mode runs across the top, and the cell shows what the member becomes when seen from inside or through the derived class.

The base's private members appear with dotted lines because the derived class cannot reach them no matter which mode you pick. They still exist inside the object (the base needs them to do its job), but the derived class has to go through public or protected member functions of the base if it wants to read or change them.

public Inheritance: The "Is-A" Mode

public inheritance is what you use when a derived class is a kind of the base. A Discounted product is still a product. A PreorderProduct is still a product. A Customer is not a product, so Customer : public Product would be a mistake even if both happen to share a name field.

This is the mode used in the vast majority of inheritance. The base's public interface stays public on the derived class, so any code that accepts a Product will accept a Discounted too. The base's protected members stay protected, so the derived class can reach them but outside code still cannot.

The mouse object has access to everything a regular Product exposes (the public name and printName), and Discounted itself can reach into the protected price to apply the discount. The private stock member exists in the object's memory layout but Discounted::applyDiscount cannot touch it. If Discounted needed to reduce stock, it would have to call a public or protected member function of Product that does the work.

A useful way to read class Discounted : public Product out loud: "A Discounted is a Product, and the Product part of it stays as accessible as it ever was." That phrasing matches what the language does.

protected Inheritance: A Narrow Middle Ground

With protected inheritance, the base's public members become protected in the derived class, and the protected members stay protected. From outside, nobody sees the base's interface at all. From inside the derived class (or its own derived classes further down the hierarchy), the inherited members are still reachable.

From main, the entire Product interface has effectively disappeared. ip.name is a compile error, ip.printName() is a compile error, all of it. The derived class still works internally because protected members are reachable from a class's own member functions. Anything InternalProduct wants the outside world to see, it has to expose itself through new public members like setup.

The use case for this is narrow. It shows up when you have a small family of internal types that share a base, and you want the base's interface available within the family but invisible to anyone else. Day-to-day C++ work rarely needs it. Many teams treat protected inheritance as a smell and use composition (a Product member variable inside the new class) instead. If you find yourself wanting it, that is a signal to stop and ask whether the relationship is really "is-a".

Picking the wrong inheritance mode has zero runtime cost. The rewriting happens entirely at compile time. The cost is paid in API design: once code outside the class depends on the base interface being public, switching to protected or private inheritance becomes a breaking change that touches every caller.

private Inheritance: Composition by a Different Name

private inheritance is the strictest mode. Every public and protected member of the base becomes private in the derived class. From outside, none of the base's interface is visible. From inside the derived class, everything that was public or protected in the base is still reachable, but only as private machinery.

This is sometimes called "implemented in terms of" inheritance. The derived class is using the base as an implementation detail, not advertising any "is-a" relationship. A Cart that inherits privately from std::vector<Product> is saying "I want to use a vector internally, but I am not a vector and I do not want anyone to treat me like one".

Cart reuses everything ProductList already does (storing items, counting them, looking them up by index), but its public interface is just addItem and printSummary. Outside code cannot call add, size, or at directly through a Cart. The base class is a hidden implementation detail.

In modern C++, composition is almost always the better choice over private inheritance. Make ProductList a member variable of Cart and forward to it manually:

Composition is clearer (the relationship is a plain field), avoids the conceptual baggage of inheritance, and does not surprise readers who scan class Cart : private ProductList and wonder what is going on. The textbook reasons to prefer private inheritance over composition (needing access to protected members of the base, needing to override virtual functions) are real but rare. If neither applies, use a member variable.

What protected Buys You Inside a Derived Class

The whole reason protected exists as a separate access category is for inheritance. From outside a class, protected looks the same as private: both are unreachable. The difference shows up when a derived class's member functions try to touch the base's members.

Discounted::applyDiscount reaches into Product::price directly because price is protected in the base, and public inheritance keeps it protected in the derived class. The commented-out line tries to read stock, which is private in Product. Even though Discounted is a Product and the stock member exists inside every Discounted object, the private label hides it from the derived class's own functions.

The trade-off with protected data members is real. Marking data protected gives derived classes a backdoor into the base's state, which can make changes to the base painful: every derived class that touched a protected field is now coupled to its name and type. A common middle ground is to keep data members private and offer protected member functions that derived classes call to read or modify the state. The base can then change its storage without breaking subclasses.

Default Mode for class and struct

Like in-class default access, the default inheritance mode depends on which keyword you used to declare the derived type.

A bare class Derived : Base with no mode keyword inherits privately. A bare struct Derived : Base inherits publicly. This is easy to get wrong, because the in-class default and the inheritance default both follow the same class is private, struct is public rule, but they apply to two different things.

Because of this rule, always write the inheritance mode explicitly when you want public inheritance from a class. Writing class Discounted : public Product makes your intent obvious and matches what most C++ codebases expect. Leaving it as class Discounted : Product works in toy examples but reads like a bug to anyone who knows the language.

Style guides like Google's and the C++ Core Guidelines recommend always spelling out the mode keyword, even on struct, where the default would already be public. The few extra characters are worth removing the ambiguity.

When to Pick Which Mode

The summary is short. Most inheritance should be public. The other two modes exist for cases that come up rarely.

ModeUse it whenFrequency in real code
publicThe derived class is a kind of the base ("is-a"). Code that accepts a base should accept the derived too.Almost always.
protectedYou want a small internal hierarchy where the base interface is shared among derived classes but invisible to outside code.Rare. Often a sign that composition would be cleaner.
privateYou want to reuse the base's implementation without exposing the base interface ("implemented in terms of").Rare. Composition (a base member variable) is usually clearer.

A useful question to ask before picking protected or private inheritance: "Could I do this with a member variable instead?" If the answer is yes (and it usually is), prefer the member variable. Inheritance fits when you need polymorphism (the derived class will be used through a base pointer or reference) or when you want the "is-a" relationship to show up in the type system. For pure code reuse, composition wins on readability and on flexibility for future changes.

Two real reasons to prefer private inheritance over a member variable:

  1. The derived class needs to access protected members of the base. A member variable sees only the public interface.
  2. The derived class needs to override one of the base's virtual functions. You cannot override anything through composition alone.

Both are uncommon in beginner code.

What's Wrong With This Code?

The mistake below appears in codebases sometimes. It looks reasonable until you trace what the inheritance mode is doing.

What is wrong with this code?

Three lines fail to compile, all because of the same root cause:

  • Line (1): name was public in Product, but private inheritance made it private inside Discounted. Outside code cannot touch it.
  • Line (2): Same story for price. Private in Discounted, no access from main.
  • Line (3): The implicit conversion from Discounted& to Product& is also blocked. With private inheritance, the outside world is not allowed to treat a Discounted as a Product. The function call fails because there is no accessible base-class-shaped view.

Fix: the author wanted "a Discounted is a Product", which is the public inheritance use case. Change one keyword:

With public inheritance, the public members stay public, the implicit base conversion works, and printProduct(mouse) compiles. The lesson is that the inheritance mode is part of the type's contract, not a free knob to twiddle. Pick it based on the relationship you want, not based on what looks tidy.

Putting the Whole Picture Together

One program exercises all three modes side by side, so the differences are visible in one place.

The pattern that emerges: the inside of each derived class can reach name, price, and printName regardless of mode, because all three were either public or protected in the base, and protected stays accessible to the derived class. The outside view is what changes. With public inheritance, main sees the full base interface. With protected, the base interface vanishes from main's view. With private, same story.

stock is the only base member that nobody in any of the three derived classes can touch, because it was private in Product to begin with. The bottom row of the table holds no matter which mode you pick.

That is the entire mechanical content of access specifiers in inheritance. Three modes, one rewriting rule, one default that flips between class and struct, and one strong recommendation: write the mode explicitly and use public unless you have a clear reason not to.

Quiz

Access Control in Inheritance Quiz

10 quizzes