C++中公共虚继承接口,私有继承实现。

3
我的问题是关于以下代码为什么会表现出这种行为。我不是在询问设计的质量(我知道有些人会立即反感多重继承,我在这里不想争论赞成或反对)。我可以提出一个单独的问题,探讨作者试图实现什么,但是让我们假设存在等效于此代码的代码:-
class IReadableData
{
    public: virtual int getX() const = 0;
};

class Data : public virtual IReadableData
{
   public: 
      virtual int getX() const { return m_x; }
      void setX(int x) {m_x = x;}
   private:
      int m_x;   
};

class ReadableData : private Data, public virtual IReadableData
{
     // I'd expected to need a using here to expose Data::getX
};

这段代码在Visual Studio 2017上可以编译通过,但会出现警告C4250:“ReadableData”继承“Data::Data::getX”的方式具有优先权。
起初,我感到有点惊讶,并未被告知ReadableData没有实现getX(因为其实现是私有的),但没有警告发生,我可以创建一个ReadableData,但尽管ReadableData公开继承了IReadableData,但无法访问IReadableData的公共方法。
    ReadableData r;
    //  r.getX(); error C2247: 'Data::getX' not accessible because 'ReadableData' uses 'private' to inherit from 'Data'

然而以下内容确实编译通过。
    ReadableData r;
    IReadableData& r2 = r;
    r2.getX();

这对我来说似乎不一致,要么r是IReadableData并且getX应该可用,要么它不是,那么对r2的赋值(或ReadableData的定义)应该失败。这种情况是否应该发生?标准中的哪些内容导致了这种情况?
这个问题: 带有混合继承修饰符(protected / private / public)的菱形继承 似乎有关联,但在那种情况下基类不是抽象的,它的答案引用第11.6节使我认为r.getX();应该是可访问的。
编辑:我稍微修改了示例代码,以便稍微了解意图,但实际上并没有改变问题。

我有点困惑... 你是想说"ReadableData拥有IReadableData接口,但实现的是Data"这样的意思吗?那只是Data并且你使用了typedef。 - UKMonkey
@UKMonkey 这只是一个最简示例,不是真正的代码。 - ROX
@Ron,“避免多重继承”?接口呢?即使C#也允许这样做(尽管它不允许私有继承)。 - ROX
通常的做法是有一个Data的私有成员,然后ReadableData的实现只是包装了Data函数。继承的问题不在于多重继承,而在于它们在不应该继承时继承了。 - UKMonkey
@UKMonkey,谢谢,我同意,这也是我通常会自己做的方式。不过我仍然想了解为什么这样的代码会产生这样的行为。 - ROX
2个回答

3
这是查找规则的一个衍生物,与一些虚拟继承相关。其思想是,在名称查找期间,虚拟继承不应导致歧义。因此,在每种情况下都找到了getX()的不同声明。由于访问说明符仅在名称查找后(并且不影响名称解析)进行检查,因此您可能会遇到这样的问题。
  • 对于r2.getX();的情况,查找从IReadableData的上下文开始。由于立即找到了声明,并且IReadableData没有基类,所以它就解决了。这是IReadableData的公共成员,因此可以命名和调用它。之后,动态分派机制负责调用主导Data::getX()给出的实现。

  • 对于r.getX();的情况,查找方式不同。它从ReadableData的上下文开始。不存在getX()的声明,因此它转而查看ReadableData的直接基类。这里有些难以理解:

    1. 它检查IReadableData,并找到IReadableData :: getX()。它注意到此次查找,以及找到它的IReadableData基子对象。
    2. 它检查Data,并找到Data :: getX()。这也注意到了此次查找,并注意到找到它的Data基子对象。
    3. 现在尝试将#1和#2的查找集合合并到ReadableData的查找集合中。由于#1中的IReadableData子对象也是#2中的Data子对象(因为虚拟继承的原因),因此完全忽略#1中的所有内容。
    4. 仅剩下#3中的Data :: getX(),查找解决它。


    因此,r.getX();实际上是r.Data::getX();。这就是查找发现的内容。这也是您错误的原因所在,此时访问说明符会被检查。

我说的一切都是试图分解标准描述的过程,该过程由 [class.member.lookup] 部分描述。我不想在此引用标准,因为我觉得它对于用通俗易懂的语言解释发生了什么并没有太多帮助。但是您可以跟随链接阅读完整的规范。


谢谢,我怀疑这与查找规则有关,快速查看标准,你可能是正确的,不直接引用它 - 但当我提到标准时,这就是我想要的答案,基于它的答案。 - ROX

1
访问修饰符总是静态解析的,而不是动态解析的,即它基于静态而非动态类型。实现了 IReadableData 对象的契约严格来说是,指向该对象的指针/引用可以别名为 IReadableData,然后可以在其上有意义地调用方法 getX。毕竟,多态契约的整个重点是要使用它们,进行多态操作。当您直接使用派生对象时,并没有真正的保证或必要性。

因此,在这种意义上,允许派生对象更改访问修饰符,但然后基于静态而非动态类型解析访问修饰符,至少是与多态契约概念一致的选择。

尽管如此,在派生对象中更改访问修饰符并不是一个好主意。它没有任何优势,因为可以轻松避开它,因此没有封装好处,只会暴露这种奇怪的边缘情况。

就设计而言,我并不根本反对多重继承。然而,钻石继承是您最好避免的东西,99% 的时间都是如此。私有继承也几乎没有用处,它们更容易枚举:

  1. 对于空基类优化。
  2. 如果有人编写了一个使用虚函数作为自定义技术的类,并且您需要这个类来实现自己的类。我之所以说“别人”是因为在更现代的C ++中,这种设计很难被证明,因为现在更容易传递函数/lambda/std::function

1
但是为什么 r.getX(); 不起作用呢?它可能不会破坏多态性,但我不明白为什么它不应该在 ReadableData 中公开。 - Daniel H
@DanielH 我不确定我是否理解了问题。从机制上讲,就像我已经说过的那样:访问修饰符是静态解析的。如果你考虑一下,纯粹从性能的角度来看,在运行时检查访问限定符是疯狂的。几乎没有好处。除了检查动态类型之外,语言在这里的唯一其他选择是禁止您在派生类中更改访问限定符。为什么C++没有这样做?通常,C++选择灵活性,并且更重的OOP曾经很流行,也许它可能会更有帮助。现在不太流行了。 - Nir Friedman
是的,它们是静态解析的,这解释了为什么 ReadableData::getXIReadableData::getX 可以 有不同的访问修饰符。但是,在这个特定的例子中,为什么 ReadableData::getXprivate 而不是 public?是什么决定了它的访问修饰符? - Daniel H
@DanielH 实现来自于 Data,而 Data 是私有继承的。当你从某个东西继承时,所有数据/方法访问都被限制在继承级别上。因此,如果你从 Data 私有继承,那么你从 Data 继承的所有内容都是私有的。实际的 getX 函数来自于 Data,而不是 IReadableData。这有意义吗?或者最后一个问题可能是问题所在;为什么访问级别基于 Data 而不是 IReadableData - Nir Friedman
是的,这就是我所问的问题。为什么访问级别来自于Data而不是IReadableData?你的评论和StoryTeller的回答似乎表明这是因为实现来自于Data;这是一个公平的经验法则吗? - Daniel H

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