第16.6章 名字隐藏与 using

English

1. 派生类里的名字会隐藏基类的每一个重载

类内部的名字查找在第一个声明了该名字的类处就停下。它不会把基类的重载收集起来再挑 最好的那个。所以派生类里声明一个 f,会隐藏基类所有的 f,包括本来匹配得更好的那些。


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) 不会失败,因为 1.5 可以转换成 ::std::int_least32_t —— 它会悄悄用截断后的值调用 hiding::f(int),这比报错更糟。用一个无法转换的实参调用,编译器就会显示 候选只有一个:


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’

一个字都没提到 base::f(double),因为查找根本没走到基类。对比 第6.6章:所有重载在同一个作用域里, 编译器会给它们排序。

2. using 把基类的名字放回作用域


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)"; }
};

现在 r.f(1) 和 r.f(1.5) 都能用:using 声明让基类的 重载在查找时成为派生类的成员,然后由普通重载解析排序全部三个候选。这是“扩展重载集, 而不是替换它”的标准做法,也正是在 private 继承下重新公开某个成员的同一套机制 (第16.3章)。

关于这个声明形状的两条规则:

3. 继承构造函数

构造函数不被继承 —— 派生类构造自己的。但一个点名基类的 using 声明会继承它的 全部构造函数(特殊成员除外),省掉为每个签名手写一个转发构造函数的麻烦:


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}; 现在能用了,并且直接初始化基类。不会被继承的是基类的 拷贝/移动构造函数与析构函数:这些照常为派生类生成 (第16.2章)。

继承来的构造函数保留基类的错误契约,所以 machine m{42}; 是 noexcept 当且仅当 machine_part(int) 是 —— 当基类构造函数 会分配、因而是 throws 时,这一点很重要 (第15.8章)。

4. 完整程序


#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);
	}
}

输出:


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)

第二行是不用 using 也能碰到被隐藏重载的办法:把对象限定成基类引用。 它能用,而且是一种坏味道 —— 它意味着派生类故意破坏了接口,调用者必须知道这件事。

5. 经验法则