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

Ama*_*wal 212 c++ polymorphism overriding

考虑一下代码:

#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);
}
Run Code Online (Sandbox Code Playgroud)

得到此错误:

>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

在这里,Derived类的函数使基类中所有相同名称(非签名)的函数不可用.不知何故,C++的这种行为看起来不太好.不是多态的.

AnT*_*AnT 393

根据你问题的措辞(你使用"隐藏"这个词来判断),你已经知道这里发生了什么.这种现象被称为"名字隐藏".出于某种原因,每当有人问一个关于为什么名字隐藏发生的问题时,回复的人要么说这个叫"名字隐藏"并解释它是如何工作的(你可能已经知道),或解释如何覆盖它(你从来没有问过),但似乎没有人愿意解决实际的"为什么"问题.

这个决定,隐藏名称背后的理由,即它实际上是为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(和下)它们突然解析为另一个函数.

另一个例子在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*)
}
Run Code Online (Sandbox Code Playgroud)

如果没有这个规则,b的状态将被部分更新,导致切片.

在设计语言时,这种行为被认为是不合需要的.作为一种更好的方法,决定遵循"名称隐藏"规范,这意味着每个类都以它声明的每个方法名称的"干净工作表"开头.为了覆盖此行为,用户需要显式操作:最初是继承方法的重新声明(当前已弃用),现在显式使用using-declaration.

正如您在原始帖子中正确观察到的那样(我指的是"非多态"评论),这种行为可能被视为违反了类之间的IS-A关系.这是事实,但显然当时已经决定,最终名称隐藏将被证明是一个较小的邪恶.

  • 是的,这是对这个问题的真正答案.谢谢.我也很好奇. (22认同)
  • 这个答案是无益的,因为给出的示例在隐藏或不隐藏的情况下都是相同的:D :: foo(int)将被调用,因为它是更好的匹配,或者因为它隐藏了B:foo(int). (12认同)
  • (老答案,我知道.)现在将`nullptr`我会反对你的例子说"如果你想调用`void*`版本,你应该使用指针类型".是否有一个不同的例子,这可能是坏的? (6认同)
  • 很棒的答案!另外,实际上,如果名称搜索每次都必须一直到顶部,编译可能会慢得多. (4认同)
  • 隐藏的名字并不是真的邪恶."is-a"关系仍然存在,可通过基本接口获得.所以也许`d-> foo()`不会得到"Is-a`Base`",但是`static_cast <Base*>(d) - > foo()`*将*,包括动态调度. (3认同)
  • @DrewHall:这似乎是一个数据结构问题.编译器应足够智能,以便将派生类的基本和派生方法存储在哈希表或类似结构中,以便快速查找而不是爬行层次结构. (2认同)
  • @RichardWolf:另一种方式更有意义。如果我们有 B::foo(int) 和 D::foo(void*),那么在没有名称隐藏的情况下,对 D::foo(NULL) 的调用仍然会解析为 B::foo(int) 因为它显然是更好的匹配但是通常这不是你想要的。但坦率地说,除了“名称查找的工作原理”这一事实之外,我想不出任何好的名称隐藏合理化 - 当您在内部作用域中调用函数时,您希望使用来自该内部作用域的函数,而不是来自外部作用域的函数(可能是全球)范围。 (2认同)
  • @Destructor:哦,解释就是AnT所说的。斯特鲁斯特鲁普曾多次这样说过。(如果你想得到这些问题的答案,你应该阅读他的《C++ 设计与演化》一书。读得很好。)只有这个例子不是很有帮助,而且这个规则一般来说是有问题的。(请注意,我怎么说_“有问题”_,而不是_“错误”_。我不确定,我也不准备投入所需的时间来找出答案。但我确实发现它值得质疑。) (2认同)

Dre*_*all 43

名称解析规则表明名称查找在找到匹配名称的第一个范围内停止.此时,重载决策规则启动以找到可用功能的最佳匹配.

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

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

using Base::gogo;
Run Code Online (Sandbox Code Playgroud)

...将允许名称查找规则查找所有候选项,因此重载决策将按预期进行.

  • OP:"为什么派生类中的重写函数会隐藏基类的其他重载?" 这个答案:"因为它确实如此". (8认同)

Jar*_*Par 12

这是"按设计".在C++中,此类方法的重载解析的工作方式如下.

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

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

  • 不幸但却是真的. (4认同)