【问题标题】:When should you restrict accessibility to a virtual function in a derived class?什么时候应该限制派生类中虚函数的可访问性?
【发布时间】:2012-05-07 22:07:37
【问题描述】:

考虑以下代码:

class Base
{
public:
    virtual void Foo() {}
};

class Derived : public Base
{
private:
    void Foo() {}
};

void func()
{
    Base* a = new Derived;
    a->Foo(); //fine, calls Derived::Foo()

    Derived* b = new Derived;
//  b->Foo(); //error
    static_cast<Base*>(b)->Foo(); //fine, calls Derived::Foo()
}

我听说过两种不同的观点:

1) 让可访问性与基类相同,因为用户无论如何都可以使用 static_cast 来获得访问权限。

2) 使函数尽可能私有。如果用户需要 a->Foo() 而不是 b->Foo(),那么 Derived::Foo 应该是私有的。如果需要,它总是可以公开的。

有理由偏爱其中一个吗?

【问题讨论】:

  • 由于您提到的原因,这种设计非常违反直觉。除非您遇到只能通过这种方式解决的情况,否则我建议您不要这样做。
  • 如果您的意图是限制 直接 使用派生类(例如工厂模式),那么受保护或私有 继承 是更合适的方式(而不是限制特定的方法)

标签: c++ virtual-functions access-specifier code-design


【解决方案1】:

限制对子类型中成员的访问会破坏Liskov substitution principleSOLID 中的 L)。我一般会建议反对它。

更新:它可能“工作”,因为代码编译并运行并产生预期的输出,但如果你隐藏一个成员,你的意图可能是亚型不如原始亚型一般。这是违反原则的。如果您的意图是通过只保留 API 用户感兴趣的内容来清理子类型接​​口,请继续执行。

【讨论】:

  • 我不认为这违反了替换原则。您仍然可以在需要引用 Base 的任何地方使用对 Derived 的引用,并且它可以正常工作。
  • @BjörnPollex 我打算通过评论回复,但后来意识到答案将受益于更新。
  • +1,但是,我要提一下,它可能仅适用于 public 继承,这通常是一种建模 is_a 关系的机制。要限制对派生类的直接访问,应使用私有或受保护的继承(而不是限制对特定成员的访问)。
  • @BjörnPollex: "can still usa a reference to Derived..." - 是的,但如果您有一个模板采用该引用而不将其强制返回到 Base&amp;Base按值对象,那么您不能将Derived 对象传递给它。这些场景可能不是 LSP 的大本营,但 LSP 中的概念仍然适用,应尽可能予以尊重。使用运行时和编译时多态机制的混合,隐藏虚拟函数会不必要地阻碍 CPU/内存调整。
  • @Jon:这在很大程度上取决于您如何打包和分发代码。如果某个客户打电话给您说“需求刚刚出现”,那么您能否以足够快的速度为他们推出新版本?至少在理论上,将成员函数从 private 更改为 public 可能会破坏二进制兼容性,尽管我怀疑在实践中您几乎可以使用任何可执行格式。既然有变通的办法,我觉得要么支持,要么不支持,不要以为不支持就可以了,以后可以加。
【解决方案2】:

不是您最初问题的答案,但如果我们谈论的是类设计...

正如 Alexandrescu 和 Sutter 在他们的第 39th 规则中建议的那样,您应该更喜欢使用公共非虚拟函数和私有/受保护虚拟函数:

class Base {
public:
    void Foo(); // fixed API function

private:
    virtual void FooImpl(); // implementation details
};

class Derived {
private:
    virtual void FooImpl(); // special implementation
};

【讨论】:

  • 您可以合理地将Base::Foo 的实现放在类定义void Foo() { FooImpl(); } 中。它永远不会超过可预测的单线,这就是重点。
  • @SteveJessop 同意,如果 FooImpl() 真的只是 Foo 的一个实现。在现实生活中,它可能是 Foo 算法的一部分,具有固定结构但有一些浮动细节。有时,公司编码标准可能会禁止在类声明中实现,即使是单行代码:)
  • @SteveJessop:实际上,不!关键是您可以在调用virtual 函数之前随意更改Foo 以包括前/后处理。它总是单行的,包含一个额外的包装是毫无意义的!
  • @anxieux:内联单行代码的问题在于内联。因此,任何更改都需要所有客户端重新编译。在库中,这显然不是问题,但是当您向用户交付中间件库时,您希望能够说“只是切换到这个新库”而不是“切换到新库,哦,你需要重新编译并交付新版本的库”。这是 ABI 兼容性的问题。
  • @Matthieu:取决于模式的目的。我认为这是为了创建一个非虚拟公共接口。它具有允许前代码和后代码的副作用,这是一种半成品版本的编织点,但这与从公共接口中删除虚拟性的目标无关。 Sutter 评论“实际上它是一个更受限制的成语,其形式类似于模板方法......我已经切换到将成语称为非虚拟接口成语”。我的重点是,我认为一开始只打算 NVI,然后将行为添加到基本功能是有风险的。
【解决方案3】:

这取决于您的设计,是否要使用派生类对象访问virtual 函数。
如果不是,那么是的,最好将它们设为privateprotected

没有基于访问说明符的代码执行差异,但代码变得更干净。
一旦你限制了类的virtual函数的访问; class 的读者可以确定不会使用派生类的任何对象或指针调用它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-26
    • 2011-08-27
    • 2010-11-10
    • 2011-05-06
    • 1970-01-01
    • 2020-07-16
    相关资源
    最近更新 更多