第16.6章 名字隐藏与 using
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章)。
关于这个声明形状的两条规则:
- 它点名基类和成员:
using base::f;,不是using f;。 - 它永远不带形参列表。
using base::f;把叫f的每一个重载都拉进来;你没法只挑一个。
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. 经验法则
- 给派生类增加一个重载时,在它旁边写
using base::name;。 否则你就悄悄把所有使用派生类型的调用者那里的基类版本都删掉了。 - 被隐藏的重载不是错误,所以没有测试能发现它。靠代码评审。
- 比起“隐藏再重新暴露”,更愿意给派生操作换个名字:一个接口取决于你恰好用了哪个类型名的类, 很难用。
- 用
using base::base;,而不是手写转发构造函数;它精确复制基类的契约。 - 名字查找是按名字,不是按签名。派生类里加一个重载,隐藏的是全部。