C ++成员函数隐藏的原因

时间:2012-08-12 16:39:35

标签: c++

  

可能重复:
  name hiding and fragile base problem

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

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)时无法在Base类中查找匹配重载?

4 个答案:

答案 0 :(得分:18)

名称查找通过查看匹配名称的当前范围来工作,如果没有找到任何内容,则它会在封闭范围中查找,如果找不到任何内容,则查看封闭范围等,直到到达全局名称空间。

这不是特定于类,您在此处隐藏的名称完全相同:

#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)),因此程序会打印{{1} }。

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

因此没有技术原因无法更改名称查找规则,但是必须更改成员函数和其他类型的名称查找,这会使编译器变慢,因为他们必须继续搜索即使在当前范围内找到匹配的名称后也会命名。理想的情况是,如果当前范围内有名称,那么可能是您想要的名称。范围inner中的调用可能想要在该范围内查找名称,例如如果两个函数在同一个命名空间中,它们可能是相关的(同一模块或库的一部分),所以如果一个使用另一个的名称,它可能意味着调用同一范围内的那个。如果那不是您想要的,那么使用显式限定或使用声明来告诉编译器在该范围内应该可以看到其他名称。

答案 1 :(得分:10)

  

这是标准委员会的“特征”还是“错误”?

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

  

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

从技术上讲,编译器可以查看基类。的技术上即可。但如果确实如此,它将违反标准规定的规则。

  

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

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

  • 如果我在派生类中声明一个具有相同名称的函数,则可以通过派生类直接访问具有相同名称的基类函数

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

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

我认为这只是一种让你在拥有同名功能之前三思而后行的机制。不同的签名。

答案 2 :(得分:4)

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

您允许其工作的其他更改将是彻底改变过载的完成方式 - 您必须至少更改所采取的步骤的顺序,以及可能的步骤的详细信息他们自己。现在,编译器查找名称,然后形成一个重载集,解决重载,然后检查对所选重载的访问。

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

但是,这仍然意味着添加到基类的接口可能会导致严重的问题。如果Base最初未包含foo,并且添加了公开 foo,则main中的d.foo()调用Derived会突然做一些完全不同的事情,并且(再次)它完全不受任何人写d.foo(5.0)的控制。

要解决这个问题,你只需要对规则做一个相当根本的改变:禁止隐式转换函数参数。除此之外,您还要更改重载决策,以便在平局的情况下,函数的派生/最本地版本优于较少派生/外部范围。根据这些规则,对Derived::foo(int)的调用可能从不首先解析为{{1}}。

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

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

答案 3 :(得分:3)

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

想象一下,派生函数会使第一个函数超载。那么,以下代码是否应该生成编译错误,或使用Derived函数?

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)。它出现在第二个版本中。现在更改派生的基类的实现打破 interface

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