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


219

考虑代码:

#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.cpp:在函数“ int main()”中:
test.cpp:31:错误:没有匹配的函数可以调用`Derived :: gogo(int)'
test.cpp:21:注意:候选对象是:virtual void Derived :: gogo(int *) 
test.cpp:33:2:警告:文件末尾没有换行符
>退出代码:1

在这里,派生类的功能使基类中所有具有相同名称(不是签名)的功能黯然失色。不知何故,C ++的这种行为看起来并不正常。不是多态的。



8
辉煌的问题,我只发现了这个最近太
马特·乔伊纳

11
我认为Bjarne(从Mac发布的链接中)最好用一句话来表达:“在C ++中,跨作用域没有重载-派生类作用域不是该一般规则的例外。”
sivabudh'2

7
@Ashish该链接已断开。这是正确的(到目前为止)-stroustrup.com/bs_faq2.html#overloadderived
nsane 2015年

3
另外,想指出的是,obj.Base::gogo(7);通过调用隐藏函数仍然可以使用。
论坛用户

Answers:


406

根据问题的措辞(使用“隐藏”一词)来判断,您已经知道这里发生了什么。这种现象称为“名称隐藏”。出于某种原因,每当有人问到为什么出现名称隐藏问题时,做出回答的人要么说这称为“名称隐藏”,然后说明其工作原理(您可能已经知道),要么说明如何覆盖它(您知道从来没有问过),但似乎没人在乎解决实际的“为什么”问题。

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

例如,假设基类B具有一个成员函数foo,该成员函数采用type参数void *,并且所有对的调用foo(NULL)都被解析为B::foo(void *)。假设没有隐藏名称,这B::foo(void *)在从继承的许多不同类中都是可见的B。但是,假设在类的某些[间接,远程]后代D中定义B了一个函数foo(int)。现在,没有名字隐藏D同时具有foo(void *)foo(int)可见光和参与重载决议。foo(NULL)如果通过类型的对象进行,调用将解析为哪个函数D?它们将解析为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的显式使用。

正如您在原始帖子中正确观察到的那样(我指的是“非多态”评论),此行为可能被视为违反类之间的IS-A Relationip。这是事实,但显然当时可以确定,隐藏姓名最终被证明是一种较小的罪恶。


22
是的,这是对这个问题的真正答案。谢谢。我也很好奇
2009年

4
好答案!另外,实际上,如果每次都必须将名称搜索一直推到顶部,则编译可能会慢很多。
德鲁·霍尔

6
(我知道答案是旧的。)现在nullptr我将反对您的示例,说“如果您要调用void*版本,则应使用指针类型”。还有另一个例子可能会不好吗?
GManNickG

3
隐藏这个名字并不是真的邪恶。“是”关系仍然存在,并且可以通过基本界面使用。因此,也许d->foo()不会让你的“是一个Base”,而是static_cast<Base*>(d)->foo() ,包括动态调度。
Kerrek SB 2014年

12
此答案无济于事,因为给定的示例在隐藏或不隐藏的情况下都具有相同的行为:D :: foo(int)将被调用,原因是匹配效果更好或因为它隐藏了B:foo(int)。
理查德·沃尔夫

46

名称解析规则说,名称查找在找到匹配名称的第一个范围内停止。届时,过载解析规则将开始寻找可用功能的最佳匹配。

在这种情况下,gogo(int*)可以在Derived类范围内找到(单独),并且由于没有从int到int *的标准转换,因此查找失败。

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

using Base::gogo;

...将允许名称查找规则找到所有候选者,因此重载解析将按照您的预期进行。


10
OP:“为什么派生类中的重写函数会隐藏基类的其他重载?” 答案是:“因为有”。
理查德·沃尔夫

12

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

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

由于“派生”不具有名为“ gogo”的匹配函数,因此重载解析失败。


2

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

考虑以下代码:

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

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

Derived dobj;

如果Base::func(float)Derived::func(double)在Derived中隐藏dobj.func(0.f),则即使可以将float提升为double ,我们也会在调用时调用基类函数。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.