为什么派生类中被重写的函数隐藏基类的其他重载?

考虑下面的代码:

#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++的这种行为看起来并不好。不是多态。

50744 次浏览

这就是“设计”。在c++中,这类方法的重载解析如下所示。

  • 从引用类型开始然后到基类型,找到第一个类型它的方法名为gogo
  • 只考虑在该类型上命名为“gogo”的方法查找匹配的重载

由于Derived没有一个名为“gogo”的匹配函数,重载解析失败。

名称解析规则表示名称查找在找到匹配名称的第一个作用域内停止。这时,重载解析规则开始发挥作用,以找到可用函数的最佳匹配。

在这种情况下,gogo(int*)在派生类作用域中被(单独)找到,并且由于没有从int到int*的标准转换,查找失败。

解决方案是通过派生类中的using声明引入Base声明:

using Base::gogo;

…将允许名称查找规则找到所有候选对象,因此重载解析将按预期进行。

从你问题的措辞来看(你用了“隐藏”这个词),你已经知道这里发生了什么。这种现象被称为“隐藏姓名”。出于某种原因,每当有人问一个关于为什么名称隐藏发生的问题时,回答的人要么说这叫做“名称隐藏”;解释它是如何工作的(你可能已经知道了),或者解释如何覆盖它(你从来没有问过),但似乎没有人关心解决实际的“为什么”。的问题。

这个决定,即隐藏名称背后的基本原理,即为什么,它实际上是设计到c++中的,是为了避免某些违反直觉的、不可预见的和潜在的危险行为,如果允许继承的重载函数集与给定类中的当前重载集混合在一起,这些行为可能发生。您可能知道,在c++中,重载解析是通过从候选函数集中选择最佳函数来实现的。这是通过将参数的类型与形参的类型匹配来实现的。匹配规则有时可能很复杂,并且经常导致在毫无准备的用户看来不合逻辑的结果。向一组先前存在的函数中添加新函数可能会导致重载解析结果发生相当大的变化。

例如,假设基类B有一个成员函数foo,它接受类型为void *的形参,并且所有对foo(NULL)的调用都解析为B::foo(void *)。假设没有隐藏名称,并且这个B::foo(void *)B派生的许多不同类中可见。然而,假设在类B的某些[间接,远程]后代D中定义了一个函数foo(int)。现在,在没有隐藏名字的情况下,D同时显示了foo1和foo(int),并参与了重载解析。如果通过类型为D的对象进行调用,对foo(NULL)的调用将解析到哪个函数?它们将解析为foo5,因为foo6比任何指针类型都能更好地匹配积分0(即foo7)。因此,在整个层次结构中,对foo(NULL)的调用解析为一个函数,而在D(及以下)中,它们突然解析为另一个函数。

另一个例子在第77页的c++的设计与发展中给出:

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。

正如你在你最初的帖子中正确地观察到的那样(我指的是“不多态”;remark),这种行为可能被视为违反了类之间的IS-A关系。这是事实,但显然当时的人们认为,最终隐藏名字会被证明是一个较轻的邪恶。

名称隐藏是有意义的,因为它可以防止名称解析中的歧义。

考虑下面的代码:

class Base
{
public:
void func (float x) { ... }
}


class Derived: public Base
{
public:
void func (double x) { ... }
}


Derived dobj;

如果Base::func(float)没有被派生中的Derived::func(double)隐藏,则在调用dobj.func(0.f)时调用基类函数,即使浮点数可以升格为双精度浮点数。

参考:http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/