Ch16.6: Name Hiding and using
1. A Name in the Derived Class Hides Every Base Overload
Name lookup inside a class stops at the first class that declares a name. It does
not gather the base class's overloads and then pick the best one. So
declaring one f in a derived class hides all the base's
fs, including the ones that would have matched better.
struct base
{
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "base::f(int)"; }
[[nodiscard]] ::std::string_view f(double) const noexcept { return "base::f(double)"; }
};
struct hiding : base
{
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "hiding::f(int)"; }
};
h.f(1.5) does not fail, because 1.5 converts to
::std::int_least32_t — it silently calls
hiding::f(int) with a truncated value, which is worse than an error. Call
it with something that cannot convert and the compiler shows that only one candidate
existed:
error: invalid conversion from ‘const char*’ to ‘int_least32_t’ {aka ‘int’} [-fpermissive]
note: initializing argument 1 of ‘std::string_view hiding::f(int_least32_t) const’
Nothing mentions base::f(double), because lookup never reached the base
class. Compare that with Ch6.6, where all
the overloads lived in one scope and the compiler ranked them.
2. using Puts the Base's Names Back in Scope
struct revealing : base
{
using base::f; // bring every base::f into this scope
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "revealing::f(int)"; }
};
Now r.f(1) and r.f(1.5) both work: the
using-declaration makes the base's overloads members of the derived class
for lookup purposes, and normal overload resolution ranks all three candidates. This is
the standard way to extend an overload set instead of replacing it, and it is the same
mechanism that re-publishes a member under private inheritance
(Ch16.3).
Two rules about the shape of the declaration:
- It names the base and the member:
using base::f;, notusing f;. - It never has a parameter list.
using base::f;brings in every overload namedf; you cannot pick one.
3. Inheriting Constructors
Constructors are not inherited — a derived class builds its own. But a
using-declaration naming the base class inherits all of its
constructors except the special members, which saves writing a forwarding constructor
per signature:
class machine_part
{
protected:
::std::int_least32_t serial_{};
public:
machine_part() noexcept = default;
explicit machine_part(::std::int_least32_t s) noexcept : serial_{s} {}
};
class machine : public machine_part
{
public:
using machine_part::machine_part; // inherits machine_part(int)
};
machine m{42}; now works and initialises the base directly. What is
not inherited is the base's copy/move constructors and its destructor: those
are generated for the derived class as usual
(Ch16.2).
Inherited constructors keep the base's error contract, so
machine m{42}; is noexcept exactly when
machine_part(int) is — which matters when the base's constructor
allocates and is therefore throws
(Ch15.8).
4. The Whole Program
#include <fast_io.h>
#include <cstdint>
#include <string_view>
struct base
{
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "base::f(int)"; }
[[nodiscard]] ::std::string_view f(double) const noexcept { return "base::f(double)"; }
};
struct hiding : base
{
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "hiding::f(int)"; }
};
struct revealing : base
{
using base::f;
[[nodiscard]] ::std::string_view f(::std::int_least32_t) const noexcept { return "revealing::f(int)"; }
};
int main()
{
try
{
hiding h{};
revealing r{};
::fast_io::io::println("hiding f(1)=", h.f(1));
::fast_io::io::println("hiding through base f(1.5)=", static_cast<base const &>(h).f(1.5));
::fast_io::io::println("revealing f(1)=", r.f(1));
::fast_io::io::println("revealing f(1.5)=", r.f(1.5));
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
hiding f(1)=hiding::f(int)
hiding through base f(1.5)=base::f(double)
revealing f(1)=revealing::f(int)
revealing f(1.5)=base::f(double)
Line two is how you reach a hidden overload without using: qualify the
object as a base reference. It works, and it is a smell — it means the derived
class deliberately broke the interface, and the caller had to know that.
5. Rules of Thumb
- When you add an overload to a derived class, add
using base::name;next to it. Without it you silently removed the base's versions for every caller using the derived type. - A hidden overload is not an error, so no test will find it. Review for it.
- Prefer giving the derived operation a different name over hiding and re-exposing it; a class whose interface depends on which type you happened to name is hard to use.
- Use
using base::base;rather than forwarding constructors by hand; it copies the base's contracts exactly. - Name lookup is per name, not per signature. One overload added in a derived class hides all of them.