C++ Part I · Module 16 · Inheritance
Name Hiding and Redeclaring a Member
Chapter 16.3 measured something it deliberately did not explain. Calling setWidth through a Rectangle reference reached the base class's function even though the object was a Square with a function of exactly that name and signature — same object, same call, behaviour decided by the type of the reference. That is not dynamic dispatch and it is not overriding. The derived function did not replace the base one; it hid it, and hiding is a name-lookup rule rather than a call-dispatch rule. The two are routinely confused, partly because the syntax for them is nearly identical and partly because the difference only shows up through a base reference. This chapter separates them and then measures the sharp consequence of hiding, which is sharper than most people expect: lookup finds a name in the nearest scope that has it and stops, so a derived class that declares a single overload of a name makes every base overload of that name invisible — not ambiguous, not deprioritised, simply absent from the candidate set. The measured results are a call that had an exact match in the base failing to compile with an error that never mentions hiding, and a second call that had an exact match in the base silently converting its argument and reaching the derived function instead. One line fixes both, and the chapter closes with when redeclaring a base name is reasonable at all.
Advanced18 min readC++Part IFundamentalsInheritance
Part I · Module 16 · Chapter 16.6 · Name Hiding and Redeclaring a Member
1. The Thing Chapter 16.3 Left Unexplained
From Chapter 16.3 §3, measured on a Square whose setWidth keeps w == h:
C. and note WHICH setWidth ran:
via Square& : w=3 h=3
via Rectangle& : w=3 h=1 <-- the base version ranOne object. One call. Two outcomes, chosen by the type of the reference. Chapter 16.3 used this to make a point about design and promised the mechanism here. The mechanism is that Square::setWidth never replaced Rectangle::setWidth — it hid it, and through a Rectangle& the hidden one was never involved.
2. Two Different Rules
| Hiding | Overriding | |
|---|---|---|
requires virtual? | no | yes |
| what it affects | which names lookup finds | which function a call reaches |
| decided by | the static type of the expression | the dynamic type of the object |
| must signatures match? | no — any same-named member hides | yes — the signature must match |
| through a base reference | the base version runs | the derived version runs |
So they are answers to different questions. Hiding answers which declarations are candidates. Overriding answers which implementation executes.
Hiding is about names. Overriding is about behaviour. A derived member with the same name always hides; it overrides only if the base declared it
virtualand the signature matches.
(virtual, the dispatch that makes overriding work, and the override keyword that checks it are Module 17's subject — 17.2 and 17.3. This chapter needs only the distinction; everything else here is hiding, which needs no virtual at all.)
3. Hiding Is Per-Name, Not Per-Signature
This is the part that surprises people. A base with three overloads:
struct Base {
void log(int n) { /* ... */ }
void log(const char* s) { /* ... */ }
void log(double d) { /* ... */ }
};A. the base alone, three overloads:
Base::log(int) 42
Base::log(char*) text
Base::log(double) 2.5Now a derived class adds one overload, with a signature matching none of them:
struct Hiding : Base {
void log(unsigned u) { /* ... */ }
};B. Hiding:
Hiding::log(unsigned) 7And h.log("text") — which has an exact match in the base — does not compile:
error: cannot initialize a parameter of type 'unsigned int' with an lvalue of type 'const char[5]'
note: passing argument to parameter hereRead that error carefully, because it is the trap. It does not say "hidden". It does not mention Base. It reads as an ordinary type error, as though log(unsigned) were the only log that ever existed — which, for lookup purposes, it is.
The rule: name lookup walks outward from the innermost scope and stops at the first scope containing the name. It found log in Hiding, so it never looked in Base. The three base overloads are not ranked lower or considered ambiguous — Chapter 6.7's overload resolution never sees them, because resolution runs after lookup has produced the candidate set, and they are not in it.
4. The Silent Case
The failure above is loud. This one is not, and it is the one worth memorising:
struct Base { void log(int n) { std::printf("Base::log(int) %d\n", n); } };
struct Hiding : Base { void log(unsigned u) { std::printf("Hiding::log(unsigned) %u\n", u); } };
Hiding h;
h.log(42); // 42 is an int. Base::log(int) is an exact match. Hiding::log(unsigned) 42The derived function ran. 42 is an int, Base::log(int) is an exact match for it, and the call went to Hiding::log(unsigned) instead — converting the argument on the way.
No error. No warning at any level. The call compiles, runs, and reaches a different function from the one a reader would predict. And because int-to-unsigned is a value-preserving conversion for 42, nothing looks wrong — until the argument is negative, at which point Chapter 3.4 §5's conversion rules turn -1 into a very large unsigned and the behaviour diverges silently.
This is why §3's rule matters in practice. The loud case costs you five minutes of confusion over an error message. The silent case costs you a wrong function call that no tool reports.
5. Data Members Hide Too
The chapter's title says member, not function, and the rule is indifferent to which. A derived class declaring a field whose name the base already uses hides it:
struct Base { int value = 11; };
struct Derived : Base { int value = 22; }; // same NAME as the base's memberA. both fields exist and both occupy space:
sizeof(Base)=4 sizeof(Derived)=8
B. which one does `d.value` name?
d.value = 22
d.Base::value = 11
C. and through a base reference?
b.value = 11 <-- the BASE field
D. writing through one does not touch the other:
after d.value = 99 : d.value=99 d.Base::value=11There are two independent fields. sizeof(Derived) is 8, not 4 — the base's value was not replaced, it is still there, still occupying its own storage, and still what a Base& sees. Writing 99 through d.value left d.Base::value at 11.
So an object can carry two differently-valued members of the same name, with which one you get decided by the static type of the expression used to reach it. That is the §3 lookup rule with a field instead of a function, and it is almost never intentional.
One genuinely useful asymmetry here: unlike function hiding, this is diagnosable.
warning: non-static data member 'value' of 'Derived' shadows member inherited
from type 'Base' [-Wshadow-field]-Wshadow-field names it exactly. It is not in -Wall -Wextra -Wpedantic — that combination gave zero warnings — so it has to be enabled deliberately, and it is worth doing: a shadowed field is nearly always a mistake, where a shadowed function name is sometimes deliberate.
6. The One-Line Fix
Re-import the base's overloads with a using-declaration:
struct Fixed : Base {
using Base::log; // bring every Base::log into scope
void log(unsigned u) { /* ... */ }
};C. Fixed, with `using Base::log;`:
Fixed::log(unsigned) 7
Base::log(int) 42
Base::log(char*) text
Base::log(double) 2.5All four overloads are now candidates, and resolution picks the best match in the ordinary way — so log(42) reaches Base::log(int) and log(7u) reaches the derived one.
using Base::log; adds the base's declarations to the derived class's scope, which is exactly what lookup needed. The rule that follows is simple:
If a derived class declares any member with the same name as a base member, add
using Base::name;unless you deliberately intend to hide.
Chapter 16.5 §3 used the same construct in its constructor form, using Base::Base;.
7. Why the Language Works This Way
The behaviour looks like an oversight and is a deliberate choice, which is worth knowing because it tells you what hiding is for.
Consider the alternative. If base overloads were always candidates, then adding an overload to a base class could silently change which function existing derived-class calls resolve to — a base-class author could break derived classes by adding something. Hiding means a derived class's own declarations take precedence, so a derived class is insulated from new overloads appearing upstream.
That is a real benefit, and it has a name worth recognising: it protects against the fragile base class problem, which Chapter 16.8 §2 treats more generally.
So the design trade is: derived classes are protected from upstream additions, at the cost of §4's silent redirection. Given the trade, the practical response is §6's one line — opt back in, explicitly, when you want the base's overloads.
8. Deliberately Replacing a Base Name
Sometimes hiding is what you want, and three cases are legitimate.
A derived class genuinely narrows an operation. A type whose resize accepts only powers of two may want resize(unsigned) and nothing else. Note the warning sign — Chapter 16.3 §7 established that narrowing a precondition fails substitutability, so this is usually a sign the derivation is wrong rather than a reason to hide.
A different return type or stronger guarantee, where the derived version should be preferred at every call that can see it.
And calling the base version explicitly, which is how a derived function wraps rather than replaces:
struct Verbose : Base {
void log(int n) {
std::printf("[verbose] ");
Base::log(n); // the qualified call that is not hidden
}
};Base::log(n) names the function directly and bypasses lookup entirely, which is the use Chapter 16.2 §8 flagged as legitimate. This is also how an override calls the base implementation — a pattern Module 17 uses constantly.
What is not legitimate is hiding by accident, which is the case §3 and §4 measured and which accounts for nearly all of it in practice.
9. How the Two Interact
One combination worth being precise about, because it is where hiding and overriding meet.
A derived function with the same name and a different signature from a base virtual function hides it — it does not override it, because overriding requires the signature to match. So you get neither dispatch nor the base overload:
struct Base { virtual void run(int); };
struct Derived : Base { void run(unsigned); }; // HIDES, does not overrideDerived::run(unsigned) is a new function. Base::run(int) is still virtual and still dispatchable through a base reference, but through a Derived& the name run finds only the derived one. Two functions, one name, no relationship.
That is a mistake frequent enough that C++11 added a keyword to catch it — writing override on a function that does not actually override anything is a compile error. Chapter 17.3 is that keyword, and it is the single best defence against this combination; it is listed here so the problem has a name before the solution arrives.
10. What to Actually Do
Treat any same-named derived member as hiding — the signature is irrelevant to that.
Add using Base::name; whenever you add an overload to a name the base already has.
Decide explicitly between override, extend, and replace before writing the member.
Call Base::name(...) explicitly when wrapping rather than replacing.
Expect no diagnostic — measured, the silent redirection draws nothing at any level.
Read a type error on a derived-class call as a possible hiding error — the message will not say so.
And treat "I want to hide this base operation" as a signal that Chapter 16.3's test may have failed.
11. Mistakes Worth Making Once
Assuming hiding is per-signature. Measured: one derived overload hid all three base overloads.
Expecting the error to mention hiding. Measured: cannot initialize a parameter of type 'unsigned int' — no mention of Base.
The silent redirection. Measured: h.log(42) reached Hiding::log(unsigned), not the exact-match Base::log(int), with no warning.
Believing that redeclaring replaces. It hides; the base function still exists and still runs through a base reference.
Thinking you need virtual for hiding. You do not — hiding needs nothing.
Expecting a different signature to override a virtual. It hides instead, giving two unrelated functions with one name.
Forgetting using Base::name; after adding an overload.
Hiding a base operation to narrow it. Chapter 16.3 §7 — that fails substitutability.
12. Interview Relevance
"What is the difference between hiding and overriding?" Hiding is a name-lookup rule needing no virtual, decided by the static type; overriding is a dispatch rule needing virtual and a matching signature, decided by the dynamic type. Having both halves is the discriminator.
"If a derived class adds one overload of a base name, what happens to the others?" They become invisible — measured, all three gone. Lookup stops at the first scope containing the name, so resolution never sees them.
"What does the error look like?" An ordinary type error that never mentions hiding — which is why this is hard to diagnose.
"Can it fail silently?" Yes, and that is the important case: measured, a call with an exact match in the base converted its argument and reached the derived overload, with no warning.
"How do you fix it?" using Base::name; in the derived class — it changes lookup, not resolution.
"Why does C++ work this way?" So a base-class author cannot break derived classes by adding an overload. The trade is deliberate.
"What if a derived function has the same name as a base virtual but a different signature?" It hides rather than overrides — two unrelated functions. override catches it, which is what the keyword is for.
13. What You Should Be Able to Say Now
- Chapter 16.3's base-version-ran result was hiding, not dispatch.
- Hiding affects which names lookup finds; overriding affects which implementation a call reaches.
- Hiding needs no
virtualand ignores signatures; overriding needs both. - Hiding is decided by the static type; dispatch by the dynamic type.
- Hiding is per-name, not per-signature — measured, one derived overload hid three base overloads.
- Lookup walks outward and stops at the first scope with the name, so the base overloads are never candidates.
- Measured:
h.log("text")gavecannot initialize a parameter of type 'unsigned int'— no mention of hiding or ofBase. - Measured, the silent case:
h.log(42)reachedHiding::log(unsigned)despiteBase::log(int)being an exact match, with no warning at any level. - An inexact match among the candidates beats a perfect match that is not a candidate.
using Base::log;is the whole fix — measured, all four overloads became candidates.- It works by changing lookup, not resolution.
- The design is deliberate: derived classes are insulated from overloads added to a base later.
- Qualified
Base::name(...)bypasses lookup and is how a derived function wraps a base one. - A different signature from a base
virtualhides rather than overrides — two unrelated functions, one name. overrideexists to catch that, and is Chapter 17.3's subject.- Decide between override, extend, and replace before writing a member whose name already exists.
14. Exercises
Exercise 1 — the two rules. Fill in the five-row comparison table from memory.
Exercise 2 — predict the candidates. Base has f(int), f(double), f(const char*); Derived adds f(std::string). List which calls compile on a Derived and which do not.
Exercise 3 — read the error. Explain why the diagnostic for a hidden call never mentions Base, in terms of compiler phases.
Exercise 4 — the silent case. Say what d.f(42) does when Base has f(int) and Derived has f(unsigned), and what warns.
Exercise 5 — fix it. Add the one line and say which phase it affects.
Exercise 6 — wrap, don't replace. Write a derived log(int) that prefixes output and then calls the base version.
Exercise 7 — hide or override. For each, say which happens: same name and signature, base not virtual; same name and signature, base virtual; same name, different signature, base virtual; different name entirely.
Exercise 8 — the design rationale. Explain what would go wrong if base overloads were always candidates.
Exercise 9 — three intentions. For a derived member whose name exists in the base, write the code for each of override, extend, and deliberately replace.
15. Summary
- Chapter 16.3's "the base version ran" was hiding, not dispatch.
- Hiding is a lookup rule (no
virtual, signature-insensitive, static type); overriding is a dispatch rule (virtual, matching signature, dynamic type). - Hiding is per-name — measured, one derived overload made all three base overloads invisible.
- Lookup stops at the first scope containing the name, so resolution never sees the base's declarations.
- Measured: the error is an ordinary type error that never mentions hiding.
- Measured, the dangerous case: a call with an exact match in the base silently converted and reached the derived overload, with no warning at any level.
using Base::name;is the whole fix, and it works by changing lookup.- The design is deliberate — it stops a base author breaking derived classes by adding an overload.
- A different signature from a base
virtualhides rather than overrides, which is whatoverrideexists to catch.
Six chapters have been about making inheritance work, and the running conclusion of all of them has been the same: most of the time you did not want it. Chapter 16.1 measured a stack that gained three operations it could not defend, at exactly the same size as the version that contained a vector. Chapter 16.3 measured a hierarchy that could satisfy the base's contract or the derived class's invariant but never both. Chapter 16.4 measured a base class losing its invariant to a three-line derived class. In each case the alternative was to hold a member instead, and it was never argued for properly — it was mentioned as the thing you should have done. The next chapter is the argument, with the decision stated as a procedure rather than a preference: what containment gives you that derivation does not, what it costs, the two things only inheritance can do, and the measured answer to the objection people raise against it, which is that forwarding functions must surely cost something.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
