考虑代码:
#include <stdio.h>
class Base {
public:
virtual void gogo(int a){
printf(" Base :: gogo (int) \n");
};
virtual void gogo(int* a){
printf(" Base :: gogo (int*) \n");
};
};
class Derived : public Base{
public:
virtual void gogo(int* a){
printf(" Derived :: gogo (int*) \n");
};
};
int main(){
Derived obj;
obj.gogo(7);
}
得到这个错误:
>g++ -pedantic -Os test.cpp -o test test.cpp: In function `int main()': test.cpp:31: error: no matching function for call to `Derived::gogo(int)' test.cpp:21: note: candidates are: virtual void Derived::gogo(int*) test.cpp:33:2: warning: no newline at end of file >Exit code: 1
在这里,派生类的函数使基类中所有同名(非签名)的函数相形见绌。不知何故,C++ 的这种行为看起来不太好。不是多态的。
obj.Base::gogo(7);
通过调用隐藏函数仍然有效。
从您问题的措辞来看(您使用了“隐藏”一词),您已经知道这里发生了什么。这种现象被称为“名字隐藏”。出于某种原因,每次有人问为什么会发生名称隐藏时,回答的人要么说这称为“名称隐藏”并解释它是如何工作的(您可能已经知道),或者解释如何覆盖它(您从来没有问过),但似乎没有人关心解决实际的“为什么”问题。
决定,名称隐藏背后的基本原理,即为什么它实际上被设计到 C++ 中,是为了避免某些违反直觉的、不可预见的和潜在危险的行为,如果允许继承的重载函数集与当前混合,则可能发生这种行为。给定类中的一组重载。您可能知道,在 C++ 中,重载解析通过从候选集中选择最佳函数来起作用。这是通过将参数类型与参数类型匹配来完成的。匹配规则有时可能很复杂,并且经常导致可能被毫无准备的用户认为不合逻辑的结果。向一组先前存在的函数添加新函数可能会导致重载解决结果发生相当大的变化。
例如,假设基类 B
有一个成员函数 foo
,它接受类型为 void *
的参数,并且对 foo(NULL)
的所有调用都解析为 B::foo(void *)
。假设没有隐藏名称,并且此 B::foo(void *)
在从 B
派生的许多不同类中可见。但是,假设在类 B
的某些 [indirect, remote] 后代 D
中定义了函数 foo(int)
。现在,如果没有名称隐藏,D
将同时显示 foo(void *)
和 foo(int)
并参与重载决议。如果通过 D
类型的对象进行调用,对 foo(NULL)
的调用将解析到哪个函数?它们将解析为 D::foo(int)
,因为 int
比任何指针类型都更适合整数零(即 NULL
)。因此,在整个层次结构中,对 foo(NULL)
的调用解析为一个函数,而在 D
(及以下)中,它们突然解析为另一个函数。
The Design and Evolution of C++, page 77 中给出了另一个例子:
class Base {
int x;
public:
virtual void copy(Base* p) { x = p-> x; }
};
class Derived : public Base{
int xx;
public:
virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};
void f(Base a, Derived b)
{
a.copy(&b); // ok: copy Base part of b
b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}
如果没有这条规则,b 的状态将被部分更新,从而导致切片。
在设计语言时,这种行为被认为是不可取的。作为一种更好的方法,决定遵循“名称隐藏”规范,这意味着每个类都以关于它声明的每个方法名称的“干净表”开头。为了覆盖此行为,需要用户进行显式操作:最初是重新声明继承的方法(当前已弃用),现在显式使用 using-declaration。
正如您在原始帖子中正确观察到的那样(我指的是“非多态”评论),这种行为可能被视为违反类之间的 IS-A 关系。这是真的,但显然当时决定最终隐藏名字将被证明是一种较小的邪恶。
名称解析规则说名称查找在找到匹配名称的第一个范围内停止。那时,重载解决规则开始寻找可用函数的最佳匹配。
在这种情况下,在 Derived 类范围内(单独)找到 gogo(int*)
,并且由于没有从 int 到 int* 的标准转换,因此查找失败。
解决方案是通过 Derived 类中的 using 声明引入 Base 声明:
using Base::gogo;
...将允许名称查找规则找到所有候选者,因此重载解决方案将按您的预期进行。
这是“设计”。在 C++ 中,此类方法的重载解析的工作方式如下。
从引用的类型开始,然后转到基类型,找到第一个具有名为“gogo”的方法的类型
仅考虑在该类型上名为“gogo”的方法找到匹配的重载
由于 Derived 没有名为“gogo”的匹配函数,因此重载解析失败。
名称隐藏是有意义的,因为它可以防止名称解析中的歧义。
考虑这段代码:
class Base
{
public:
void func (float x) { ... }
}
class Derived: public Base
{
public:
void func (double x) { ... }
}
Derived dobj;
如果 Base::func(float)
没有被 Derived 中的 Derived::func(double)
隐藏,我们将在调用 dobj.func(0.f)
时调用基类函数,即使可以将浮点数提升为双精度数。
参考:http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/
不定期副业成功案例分享
nullptr
我会反对你的例子,说“如果你想调用void*
版本,你应该使用指针类型”。有没有一个不同的例子,这可能是坏的?d->foo()
不会为您提供“Is-aBase
”,但static_cast<Base*>(d)->foo()
会,包括动态调度。