代码之家  ›  专栏  ›  技术社区  ›  Aman Aggarwal

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

  •  202
  • Aman Aggarwal  · 技术社区  · 15 年前

    考虑代码:

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

    4 回复  |  直到 6 年前
        1
  •  381
  •   Jason AnT    6 年前

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

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

    例如,假设基类 B 具有成员函数 foo 它接受类型为的参数 void * 以及所有呼叫 foo(NULL) 决心 B::foo(void *) . 假设没有名字隐藏,这个 B:FO(空洞*) 在许多不同的类中可见,从 . 但是,让我们假设在一些[间接的,遥远的]后代中 D 阶级的 函数 foo(int) 定义。现在,没有名字隐藏 D 两者都有 foo(void *) FO(int) 可见并参与过载分辨率。调用哪个函数 FO(空) 如果通过类型的对象生成,则解析为 D ?他们决心 D::foo(int) ,因为 int 是积分零(即 NULL )比任何指针类型。因此,在整个层次结构中, foo(空) 在中时解析为一个函数 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声明。

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

        2
  •  43
  •   Drew Hall    15 年前

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

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

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

    using Base::gogo;
    

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

        3
  •  13
  •   JaredPar    15 年前

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

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

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

        4
  •  2
  •   Sandeep Singh    6 年前

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

    考虑此代码:

    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_hiding_cxx/