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

问题描述 投票:0回答:1

考虑代码:

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

c++
1个回答
393
投票

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

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

例如,假设基类B具有成员函数foo,该成员函数使用类型为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(及以下)中,它们突然解析为另一个函数。

[,第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。这是事实,但显然当时可以确定,最终隐藏姓名被证明是一种较小的罪恶。


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

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

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

using Base::gogo;

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

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)隐藏,在调用dobj.func(0.f)时我们将调用基类函数,即使浮点数可以升为double。

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

    © www.soinside.com 2019 - 2024. All rights reserved.