名称隐藏和脆弱的基础问题

name hiding and fragile base problem

本文关键字:问题 脆弱 隐藏      更新时间:2023-10-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()不同,因为它是重载。

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

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 层层次结构:

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 属性来检查方法是否确实是基本虚拟的覆盖;虽然它不能解决眼前的问题,但将来我们可能会想象编译器对"意外"覆盖(即,未标记为这样的覆盖)发出警告,在这种情况下,在引入虚拟方法后的编译时可能会发现这样的问题。

The Design and Evolution of C++, Bjarne Stroustrup Addison-Weslay, 1994 Section 3.5.3 pp 77, 78, B.S. 解释说,派生类中的名称在其基类中隐藏相同名称的所有定义的规则是旧的,可以追溯到带有类的 C 中。当它被引入时,B.S.认为它是范围规则的明显结果(对于嵌套代码块或嵌套命名空间也是如此 - 即使命名空间是在之后引入的)。它与重载规则交互的可取性(重载集不包含基类中定义的函数,也不包含在封闭块中 - 现在无害,因为在块中声明函数是老式的 - 也不包含在问题偶尔发生的封闭命名空间中)一直存在争议,以至于 G++ 实现了允许重载的替代规则, B.S.认为当前的规则有助于防止以下情况下的错误(灵感来自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)。变量 b 是隐式参数 XX::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++参考手册中的规则在这里会有什么不同。