名称隐藏和脆弱的基础问题
name hiding and fragile base problem
我看到有人说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::foo
和Top::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
。另一方面,变量a
与X::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++参考手册中的规则在这里会有什么不同。
- 警告处理为错误这里有什么问题
- 最小硬币更换问题(自上而下方法)
- 为"adjacent"变量赋值时出现问题
- 我的神经网络不起作用 [XOR 问题]
- 在Ubuntu 16.04上安装Cilk时出现问题
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 编译包含字符串的代码时遇到问题
- Project Euler问题4的错误解决方案
- 问题:什么是QAbstractItemView::NoEditTriggers的反面
- 在编译C++代码(具有dlib和opencv)到WASM时面临问题
- 在进程中对同一管道进行读取和写入时C++管道出现问题
- 静态数据成员的问题-修复链接错误会导致编译器错误
- C++ 雷神库 - 使用资源加载器类时出现问题(不命名类型)
- 一个关于在C++中重载布尔运算符的问题
- 首要问题的答案让值班员搞错了
- setlocale的C++土耳其字符串问题
- 如何重构类层次结构以避免菱形问题
- 基于boost的程序的静态链接——zlib问题
- C++格式化输出问题
- 名称隐藏和脆弱的基础问题