为什么派生类中的重写函数会隐藏基类的其他重载?
-
06-07-2019 - |
题
考虑代码:
#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++ 的这种行为看起来不太好。不是多态的。
解决方案
根据您问题的措辞判断(您使用了“隐藏”一词),您已经知道这里发生了什么。该现象称为“名称隐藏”。出于某种原因,每当有人问一个关于为什么名称隐藏发生的问题时,回复的人要么说这个叫做“名字隐藏”。并解释它是如何工作的(你可能已经知道),或解释如何覆盖它(你从未问过),但似乎没有人愿意解决实际的“为什么”。问题
决定,名称隐藏背后的基本原理,即为什么它实际上被设计到C ++中,是为了避免在继承的重载集合时可能发生的某些违反直觉,无法预料和潜在危险的行为允许函数与给定类中的当前重载集混合。您可能知道在C ++中,重载决策的工作原理是从候选集中选择最佳函数。这是通过将参数类型与参数类型相匹配来完成的。匹配规则有时可能很复杂,并且通常会导致结果可能被无准备的用户视为不合逻辑。向一组先前存在的函数添加新函数可能会导致重载决策结果发生相当大的变化。
例如,假设基类 B
有一个成员函数 foo
,它带有 void *
类型的参数,以及所有调用到 foo(NULL)
被解析为 B :: foo(void *)
。假设没有名称隐藏,这个 B :: foo(void *)
在从 B
开始的许多不同类中可见。但是,假设在类 B
的某些[间接,远程]后代 D
中定义了函数 foo(int)
。现在,没有名称隐藏 D
, foo(void *)
和 foo(int)
可见并参与重载解析。如果通过 D
类型的对象进行, foo(NULL)
的调用将解析为哪个函数?他们将解析为 D :: foo(int)
,因为 int
比任何指针类型更好地匹配整数零(即 NULL
) 。因此,在整个层次结构中,对 foo(NULL)
的调用解析为一个函数,而在 D
(及其下)中,它们会突然解析为另一个函数。
另一个例子在 C ++的设计和演变,第77页中给出:
class Base {
int x;
public:
virtual void copy(Base* p) { x = p-> x; }
};
class Derived{
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。
正如您在原始帖子中正确观察到的那样(我指的是“Not polymorphic”评论),这种行为可能被视为违反了类之间的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)
,即使可以将float提升为double。
参考: http://bastian.rieck.ru/blog/posts/ 2016 / name_hiding_cxx /