关于c ++:为什么派生类中的重写函数会隐藏基类的其他重载?

Why does an overridden function in the derived class hide other overloads of the base class?

考虑代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

class Base {
public:
    virtual void gogo(int a){
        printf(" Base :: gogo (int)
"
);
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*)
"
);
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*)
"
);
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

出现此错误:

1
2
3
4
5
6
>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)。现在,没有名字隐藏Dfoo(void *)foo(int)都是可见的,并且参与过载解决。如果通过D类型的对象进行调用,那么对foo(NULL)的调用将解析为哪个函数?它们将解析为D::foo(int),因为与任何指针类型相比,int更适合于整数零(即NULL)。因此,在整个层次结构中,调用foo(NULL)解析一个函数,而在D及以下的函数中,它们突然解析为另一个函数。

另一个例子是在C++的设计和演化中,第77页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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声明。

正如您在最初的文章中正确地观察到的(我指的是"非多态性"的评论),这种行为可能被视为违反IS——类与类之间的关系。这是真的,但很明显,在当时,人们决定,以最终的名义,隐藏将被证明是一种较小的邪恶。


名称解析规则表示名称查找将停止在找到匹配名称的第一个作用域中。此时,过载解决规则开始寻找可用功能的最佳匹配。

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

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

1
using Base::gogo;

…将允许名称查找规则查找所有候选项,因此重载解决方案将按预期进行。


这是"按设计"。在C++中,这种类型的方法的超载分辨率如下所示。

  • 从引用的类型开始,然后转到基类型,找到第一个具有名为"gogo"的方法的类型
  • 只考虑该类型上名为"gogo"的方法,找到匹配的重载

由于派生函数没有名为"gogo"的匹配函数,因此重载解析失败。


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

考虑此代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)时,我们会调用基类函数,即使一个float可以提升为double。

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