为什么在重写虚函数时不考虑访问限定符?

5
以下代码打印出"I'm B!"。这有点奇怪,因为B::foo()是私有的。关于A* ptr,我们可以说它的静态类型是A(foo是公共的),动态类型是B(foo是私有的)。因此,我可以通过指向A的指针调用foo。但是这样我就能访问B中的私有函数。这是否可以被视为封装违规?
由于访问限定符不是类方法签名的一部分,因此可能会导致这种奇怪的情况。为什么在C++中覆盖虚函数时不考虑访问限定符?我能否禁止这种情况?这个决定背后的设计原则是什么? 实时示例
#include <iostream>

class A
{
public:
    virtual void foo()
    {
        std::cout << "I'm A!\n";
    };
};

class B: public A
{
private:
    void foo() override
    {
        std::cout << "I'm B!\n";
    };
};

int main()
{
    A* ptr;
    B b;

    ptr = &b;

    ptr->foo();
}

2
你正在调用 A::foo(),这是公共的。你不能直接调用 b.foo()。最重要的问题是为什么 B::foo 是私有的? - Bo Persson
假设您有一个函数 void f(A* a) { a->foo(); } 是在另一个编译的文件中。那么,编译器如何知道您是否稍后传递的是 B, C 还是 A?接口声明该函数为公共函数,因此它就是公共函数。 - Bo Persson
1
你正在调用公共的 A::foo 方法。由于没有封装违规发生,因此这不是封装违规。即使 A::foo(非虚拟)调用了一些私有方法或修改了一些私有字段,也不会发生封装违规。如果该方法被保留为公共方法,但执行了外部代码不应被允许的操作,则可能出现封装违规。 - user7860670
1
@Viktor 为什么你想要一个警告?这个情况非常好,我没有看到任何问题。如果你在那里有问题,那可能是你设计上的缺陷。 - user0042
1
没有需要捕获的错误。在class final特性出现之前,我故意使用这种东西。 - user0042
显示剩余11条评论
2个回答

3
您有多个问题,我会逐一回答。
为什么在C++中当虚函数被覆盖时不考虑访问限定符?
因为访问限定符是在所有重载决议之后由编译器考虑的。这种行为是由标准规定的。
例如,请参见cppreference上的
“成员访问不影响可见性:私有成员和私有继承的成员名称是可见的,并且通过重载决议进行考虑,对于不可访问的基类的隐式转换仍然会被考虑等等。在任何给定的语言结构被解释之后,成员访问检查是最后一步。该规则的目的是将任何私有替换为公共都不会改变程序的行为。”
下一段描述了您的示例所示的行为:
“虚函数名称的访问规则在调用点使用用于表示调用成员函数的对象的类型进行检查。忽略最终覆盖者的访问。”
另请参见此答案中列出的操作序列

我能禁止这种情况吗?

不行。

我认为你永远也做不到,因为这种行为并没有违法。

这个决定背后的设计原则是什么?

澄清一下:这里的“决定”指的是编译器在重载决议后检查访问限定符的规定。 简短的答案是:防止修改代码时出现意外情况。

更详细地说,假设你正在开发一个名为CoolClass的类,它看起来像这样:

class CoolClass {
public:
  void doCoolStuff(int coolId); // your class interface
private:
  void doCoolStuff(double coolValue); // auxiliary method used by the public one
};

假设编译器可以根据公共/私有说明符进行重载决策。那么以下代码将成功编译:
CoolClass cc;
cc.doCoolStuff(3.14); // invokes CoolClass::doCoolStuff(int)
  // yes, this would raise the warning, but it can be ignored or suppressed 

然后在某个时候,您会发现您的私有成员函数实际上对类客户端很有用,并将其移动到“公共”区域。这自动更改了现有客户端代码的行为,因为现在它调用CoolClass :: doCoolStuff(double)。
因此,应用访问限定符的规则是以不允许这种情况的方式编写的,因此您将在最初得到“模棱两可的调用”编译器错误。出于同样的原因,虚函数也不是特殊情况(请参见this answer)。
这是否可以被视为封装违规?
不完全是。 通过将指向您的类的指针转换为指向其基类的指针,您实际上是在说:“特此声明,我想使用此对象B,就好像它是对象A”-这是完全合法的,因为继承意味着“按原样”关系。
因此,问题更多地是,您的示例是否可以被认为是违反基类规定的契约?看起来是的。
有关替代解释,请参见this question的答案。

P.S.

不要误解,这并不意味着你不应该使用私有虚函数。相反地,通常被认为是一种良好的实践,请参见this thread。但它们应该从基类开始就是私有的。因此,底线是,你不应该使用私有虚函数来打破公共契约。

P.P.S. ……除非你故意想通过指向接口/基类的指针强制客户端使用你的类。但是,有更好的方法,我相信这些讨论超出了本问题的范围。


2
访问限定符如publicprivate等是编译时的特性,而动态多态是运行时的特性。
当调用虚函数的private重载时,您认为在运行时应该发生什么?会抛出异常吗?

这是否被视为封装违规?

不是,因为通过继承已经发布了接口,所以不算违规。
在派生类中使用private函数重载基类中的public virtual函数是完全正常的(也可能是有意为之)。

在这种情况下,编译时可能会出现警告已经足够了。 - Viktor
@Viktor 为什么要警告或者类似的提示?这是完全没有问题的。 - user0042
@Viktor 耸肩,不同的语言? - user0042
在C++中,这并不是直观的。也许编译过程中的一些主要限制可以解释原因? - Viktor
@Viktor 如您所述,这不是一个错误。没有进一步的解释(可以将其视为一项功能)。 - user0042
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接