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

Posted

技术标签:

【中文标题】为啥派生类中的重写函数会隐藏基类的其他重载?【英文标题】:Why does an overridden function in the derived class hide other overloads of the base class?为什么派生类中的重写函数会隐藏基类的其他重载? 【发布时间】:2009-10-27 04:24:02 【问题描述】:

考虑代码:

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

【问题讨论】:

复制:***.com/questions/411103/… 好问题,我也是最近才发现的 我认为 Bjarne(来自 Mac 发布的链接)用一句话说得最好:“在 C++ 中,没有跨范围的重载 - 派生类范围也不例外。” @Ashish 该链接已损坏。这是正确的(截至目前)-stroustrup.com/bs_faq2.html#overloadderived 另外,想指出 obj.Base::gogo(7); 仍然可以通过调用隐藏函数来工作。 【参考方案1】:

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

决定,隐藏名称背后的基本原理,即为什么它实际上被设计成 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(及以下)中,它们突然解析为另一个函数。

The Design and Evolution of C++,第 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 关系。这是真的,但显然当时决定最终隐藏姓名将被证明是一种较小的邪恶。

【讨论】:

是的,这是对这个问题的真实答案。谢谢你。我也很好奇。 很好的答案!此外,实际上,如果名称搜索每次都必须一直到顶部,那么编译可能会慢很多。 (旧答案,我知道。)现在nullptr 我会反对你的例子,说“如果你想调用void* 版本,你应该使用指针类型”。有没有其他例子表明这可能是坏事? 隐藏名字并不邪恶。 “is-a”关系仍然存在,并且可以通过基本接口使用。所以也许d-&gt;foo() 不会给你“Is-a Base”,但static_cast&lt;Base*&gt;(d)-&gt;foo() ,包括动态调度。 这个答案没有帮助,因为给出的示例无论是否隐藏都表现相同: D::foo(int) 将被调用,因为它是更好的匹配或因为它隐藏了 B:foo(整数)。【参考方案2】:

名称解析规则规定名称查找在找到匹配名称的第一个范围内停止。此时,重载解析规则会启动以找到可用函数的最佳匹配。

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

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

using Base::gogo;

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

【讨论】:

OP:“为什么派生类中的重写函数会隐藏基类的其他重载?”这个答案:“因为它确实如此”。【参考方案3】:

这是“设计”。在 C++ 中,此类方法的重载解析的工作方式如下。

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

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

【讨论】:

【参考方案4】:

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

考虑这段代码:

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/

【讨论】:

以上是关于为啥派生类中的重写函数会隐藏基类的其他重载?的主要内容,如果未能解决你的问题,请参考以下文章

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

函数的重载重写与隐藏

类成员函数的重载覆盖和隐藏

C++重载隐藏和覆盖的区别

类中的同名函数关系,重载,覆盖/重写,隐藏

C++中派生类重写基类重载函数时需要注意的问题:派生类函数屏蔽基类中同名函数