27

可能重复:
名称隐藏和脆弱的基础问题

我熟悉涉及成员函数隐藏的规则。基本上,具有与基类函数同名的函数的派生类实际上并没有重载基类函数——它完全隐藏了它。

struct Base
{
    void foo(int x) const
    {

    }
};

struct Derived : public Base
{
    void foo(const std::string& s) { }
};


int main()
{
    Derived d;
    d.foo("abc");
    d.foo(123); // Will not compile! Base::foo is hidden!
}

因此,您可以通过using声明来解决此问题。但我的问题是,基类函数隐藏的原因是什么?这是标准委员会的“功能”还是仅仅是“错误”?当编译器找不到匹配的重载时,编译器无法在基类中查找匹配的重载是否存在某种技术原因d.foo(123)

4

4 回答 4

23

名称查找通过在当前范围中查找匹配名称来工作,如果没有找到,则在封闭范围中查找,如果没有找到,则在封闭范围中查找,等等,直到到达全局命名空间。

这不是特定于类的,你会得到完全相同的名称隐藏在这里:

#include <iostream>

namespace outer
{
  void foo(char c) { std::cout << "outer\n"; }

  namespace inner
  {
    void foo(int i) { std::cout << "inner\n"; }

    void bar() { foo('c'); }
  }
}

int main()
{
  outer::inner::bar();
}

虽然outer::foo(char)是更好的匹配调用foo('c')名称查找在找到后停止outer::inner::foo(int)(即outer::foo(char)隐藏),因此程序打印inner.

如果不隐藏成员函数名称,则意味着类范围内的名称查找与非类范围内的名称查找行为不同,这将不一致且令人困惑,并使 C++ 更难学习。

所以没有技术原因不能更改名称查找规则,但必须为成员函数和其他类型的名称查找更改它们,这会使编译器变慢,因为他们必须继续搜索名称,即使在在当前范围内查找匹配的名称。明智地,如果当前范围内有一个名称,它可能就是您想要的那个。范围内的调用A可能希望在该范围内查找名称,例如,如果两个函数在同一个命名空间中,它们可能是相关的(同一模块或库的一部分),因此如果一个使用另一个名称,则可能意味着在同一范围内调用一个。如果说'

于 2012-08-12T17:46:21.523 回答
10

这是标准委员会的“功能”还是仅仅是“错误”?

这绝对不是错误,因为它在标准中明确规定。这是一个特点。

当编译器找不到 d.foo(123) 的匹配项时,编译器无法在基类中查找匹配的重载是否存在某种技术原因?

从技术上讲,编译器可以查看基类。从技术上讲。但如果这样做,就会违反标准设定的规则。

但我的问题是,基类函数隐藏的原因是什么?

除非委员会有人给出答案,否则我认为我们只能推测。基本上,有两种选择:

  • 如果我在派生类中声明了同名的函数,请保持基类的同名函数可通过派生类直接访问

它可以通过掷硬币来确定(......好吧,也许不是)。

一般来说,想要一个与基类同名的函数的原因是什么?有不同的功能 - 您更有可能使用多态性。为了处理不同的情况(不同的参数),如果基类中不存在这些情况,则策略模式可能更适合处理该工作。因此,当您确实想要隐藏该功能时,最有可能的功能隐藏会生效。您对基类实现不满意,因此您提供自己的实现,并选择使用using,但仅在您需要时。

我认为这只是一种机制,让您在使用具有相同名称和不同签名的函数之前三思而后行。

于 2012-08-12T16:54:17.407 回答
5

我相信@Lol4t0 是非常正确的,但我会更强烈地陈述事情。如果你允许这样做,你最终会得到两种可能性:要么在几乎整个语言中进行大量其他更改,要么你最终得到的东西几乎完全被破坏了。

您为使其正常工作所做的其他更改将是彻底修改重载的完成方式——您至少必须更改所采取步骤的顺序,并且可能还需要更改步骤本身的细节。现在,编译器查找名称,然后形成一个重载集,解决重载,然后检查对所选重载的访问。

为了使这项工作更加顺利,您几乎必须更改它以首先检查访问,并且只将可访问函数添加到重载集。这样,至少@Lol4t0 的答案中的示例可以继续编译,因为Base::foo永远不会被添加到重载集中。

然而,这仍然意味着添加到基类的接口可能会导致严重的问题。如果Base最初没有包含foo,并且添加了一个public foo,那么调用maintod.foo()会突然做一些完全不同的事情,并且(再次)它将完全不受任何编写Derived.

要解决这个问题,您只需对规则进行相当根本的更改:禁止函数参数的隐式转换。除此之外,您将更改重载解决方案,因此在出现平局的情况下,函数的最派生/最本地版本比派生较少/外部范围更受青睐。有了这些规则,调用tod.foo(5.0)永远无法解决Derived::foo(int)

然而,这只会留下两种可能性:要么对自由函数的调用与对成员函数的调用具有不同的规则(只允许自由函数进行隐式转换),要么将完全放弃与 C 的所有兼容性(即,也禁止隐式转换在所有函数参数中,这将破坏大量现有代码)。

总结一下:要在不完全破坏语言的情况下进行更改,您还必须进行许多其他更改。几乎可以肯定,可以创建一种以这种方式工作的语言,但是当你完成时,它不会是 C++,只需稍作改动——它将是一种完全不同的语言,与 C++C不太一样,或其他任何东西。

于 2012-08-12T17:35:47.933 回答
3

我只能提议,做出这个决定是为了让事情变得更简单。

想象一下,派生函数将重载基数。那么,下面的代码应该产生编译错误,还是使用Deriveds 函数?

struct Base
{
private:
    void foo(float);
}

struct Derived: public Base
{
public:
    void foo(int);
}

int main()
{
    Derived d;
    d.foo(5.0f);
}

根据现有的重载行为,这应该会产生错误。

现在想象一下,在第一个版本Base中没有foo(float). 在第二个版本中出现。现在改变基类的实现会破坏派生的接口

如果您是开发人员Derived并且无法影响开发人员Base并且很多客户使用您的界面,那么您现在的处境很糟糕。

于 2012-08-12T17:04:54.003 回答