【问题标题】:name hiding and fragile base problem名称隐藏和脆弱的基础问题
【发布时间】:2011-08-21 04:12:16
【问题描述】:

我看到它声明 C++ 隐藏名称是为了减少脆弱的基类问题。但是,我绝对看不出这有什么帮助。如果基类引入了以前不存在的函数或重载,它可能与派生类引入的函数或重载发生冲突,或者对全局函数或成员函数的非限定调用——但我没有看到这与重载有何不同.为什么要区别对待虚函数的重载与其他任何函数?

编辑:让我再向你展示一下我在说什么。

struct base {
    virtual void foo();
    virtual void foo(int);
    virtual void bar();
    virtual ~base();
};
struct derived : base {
    virtual void foo();
};

int main() {
    derived d;
    d.foo(1); // Error- foo(int) is hidden
    d.bar(); // Fine- calls base::bar()
}

在这里,foo(int)bar() 的处理方式不同,因为它是一个重载。

【问题讨论】:

  • 有点含糊......你能澄清一下实际问题吗?
  • 这不仅仅是虚函数的问题,普通的成员函数也有这个“问题”。
  • base::foo(int) 的处理方式与base::bar() 没有区别;它只是被derived::foo() 隐藏了。如果derived 有一个名为bar 的成员,那么它将以完全相同的方式隐藏base::bar(),无论它是否也被重载。
  • @Mike Seymour:很有趣,因为派生结构没有提及任何关于foo(int) 的内容,但它的行为仍然与bar() 不同。如果那不是不同的处理方式,我不知道是什么。
  • 那么也许你应该澄清你的意思是“区别对待”,因为恐怕我很难理解你在说什么。 derived 有一个名为 foo 的成员,它隐藏了任何继承的同名成员(除非您明确地将其包含在 using 中)。它没有名为bar 的成员,因此具有该名称的继承成员仍然可见。这些都与重载或虚函数无关,据我所知,在隐藏它们时,这些东西的处理方式与其他任何东西都没有区别。

标签: c++ oop inheritance


【解决方案1】:

我假设“脆弱的基类”是指对基类的更改可能会破坏使用派生类的代码(这是我在 Wikipedia 上找到的定义)的情况。我不确定虚函数与此有什么关系,但我可以解释一下隐藏如何有助于避免这个问题。考虑以下几点:

struct A {};

struct B : public A
{
    void f(float);
};

void do_stuff()
{
    B b;
    b.f(3);
}

do_stuff 中的函数调用调用B::f(float)

现在假设有人修改了基类,并添加了一个函数void f(int);。如果不隐藏,这将更适合main 中的函数参数;您要么更改了 do_stuff 的行为(如果新函数是公共的),要么导致编译错误(如果它是私有的),而没有更改 do_stuff 或其任何直接依赖项。通过隐藏,您并没有改变行为,只有当您使用 using 声明明确禁用隐藏时,才有可能发生这种破坏。

【讨论】:

  • 引入了一个虚函数,将后续派生类中的函数转换为覆盖,从而激活它们的虚拟调度。正如我在回答中明确指出的那样,层次结构至少需要 3 级深才能显示问题。这在实践中可能非常罕见,我怀疑它是否会被任何警告检测到。
  • 对。但是这对于虚函数的重载有什么不同呢?引入任何新函数或成员也是如此,无论其名称如何。然而标准隐藏了base::foo(int),而不是base::bar()
【解决方案2】:

我认为虚函数的重载与常规函数的重载没有什么不同。不过可能会有副作用。

假设我们有 3 层层次结构:

struct Base {};

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

struct Top: Derived { void foo(int i); }; // hides Derived::foo

当我写作时:

void bar(Derived& d) { d.foo(3); }

调用静态解析为Derived::foo,无论d 可能具有的真实(运行时)类型。

但是,如果我在Base 中引入virtual void foo(int i);,那么一切都会改变。突然,Derived::fooTop::foo 变成了覆盖,而不是仅仅将名称隐藏在各自基类中的重载。

这意味着d.foo(3); 现在不是直接静态解析为方法调用,而是虚拟调度。

因此Top top; bar(top) 将调用Top::foo(通过虚拟调度),它之前调用Derived::foo

这可能是不可取的。可以通过明确限定调用 d.Derived::foo(3); 来修复它,但这肯定是一个不幸的副作用。

当然,这主要是一个设计问题。只有在签名兼容的情况下才会发生,否则我们将隐藏名称,并且没有覆盖;因此,有人可能会争辩说,对非虚拟功能进行“潜在”覆盖无论如何都会招致麻烦(不知道是否存在任何警告,它可能需要一个警告,以防止陷入这种情况)。

注意:如果我们移除 Top,那么引入新的虚拟方法是完全可以的,因为所有旧调用都已经由 Derived::foo 处理,因此只有新代码可能会受到影响

在基类中引入新的virtual 方法时需要牢记这一点,尤其是当受影响的代码未知时(交付给客户端的库)。

请注意,C++0x 具有override 属性来检查方法是否真的是基虚拟的覆盖;虽然它不能解决眼前的问题,但将来我们可能会想象编译器会警告“意外”覆盖(即未标记为这样的覆盖),在这种情况下,这样的问题可能会在编译时被捕获之后 引入虚方法。

【讨论】:

    【解决方案3】:

    C++ 的设计和演变中,Bjarne Stroustrup Addison-Weslay,1994 年第 3.5.3 节,第 77、78 页,B.S.解释了派生类中的名称在其基类中隐藏所有同名定义的规则是旧的,并且可以追溯到 C with Classes。当它被介绍时,B.S.将其视为范围规则的明显结果(对于嵌套的代码块或嵌套的命名空间也是如此——即使命名空间是在之后引入的)。它与重载规则交互的可取性(重载集不包含在基类中定义的函数,也不包含在封闭块中 - 现在无害,因为在块中声明函数是老式的 - 也不包含偶尔出现问题的封闭命名空间罢工)一直在争论,以至于 G++ 实现了允许重载的替代规则,而 BS认为当前规则有助于防止在诸如(受 g++ 实际问题的启发)等情况下出现错误

    class X {
       int x;
    public:
       virtual void copy(X* p) { x = p->x; }
    };
    
    class XX: public X {
       int xx;
    public:
       virtual void copy(XX* p) { xx = p->xx; X::copy(p); }
    };
    
    void f(X a, XX b) 
    {
       a.copy(&b); // ok: copy X part of b
       b.copy(&a); // error: copy(X*) is hidden by copy(XX*)
    }
    

    然后是 B.S.继续

    回想起来,我怀疑 2.0 中引入的重载规则可能已经能够处理这种情况。考虑调用b.copy(&a)。变量bXX::copy 隐式参数的精确类型匹配,但需要标准转换才能匹配X::copy。另一方面,变量aX::copy 的显式参数完全匹配,但需要标准转换才能匹配XX:copy。因此,如果允许重载,调用就会出错,因为它是模棱两可的。

    但我看不出歧义在哪里。在我看来,B.S.忽略了 &a 不能隐式转换为 XX* 的事实,因此只考虑了 X::copy

    确实尝试使用免费(朋友)功能

    void copy(X* t, X* p) { t->x = p->x; }
    void copy(XX* t, XX* p) { t-xx = p->xx; copy((X*)t, (X*)p); }
    

    我没有得到当前编译器的歧义错误,而且我看不出注释 C++ 参考手册中的规则在这里会有什么不同。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-25
      • 1970-01-01
      • 1970-01-01
      • 2013-12-06
      • 2013-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多