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:

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