C++ public 接口的虚拟继承与实现的私有继承

C++ public virtual inhertiance of interface with private inheritance of implementation

我的问题是为什么下面的代码会这样运行。我不是在问设计的质量(我知道你们中的一些人会立即讨厌多重继承,我在这里不是在争论或反对它)我可以问一个单独的问题来了解作者试图做的事情实现,但让我们假设有与此等效的代码:-

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 年 "warning C4250: 'ReadableData': inherits 'Data::Data::getX' via dominance"

首先,我有点惊讶没有被告知 ReadableData 没有实现 getX(假设它的实现是私有的),但是没有出现警告并且我可以创建一个 ReadableData,但是尽管 ReadableData publicly继承自IReadableData,IReadableData的public个方法不可访问

    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 的定义)的赋值应该失败。这应该发生吗?标准中的什么导致了这个?

这个问题:- Diamond inheritance with mixed inheritance modifers (protected / private / public)

似乎有联系,除了那种情况下的基础不是抽象的,它引用第 11.6 节的答案会让我认为 r.getX();应该可以访问。

编辑:我将示例代码稍微简化了一点,以便对意图有一点了解,但这并没有真正改变问题。

访问修饰符总是静态解析,而不是动态解析,即它基于静态而非动态类型。实现 IReadableData 的对象的契约严格来说是该对象的 pointer/reference 可以别名为 IReadableData,然后可以有意义地调用方法 getX它。毕竟,多态合约的全部意义在于以多态方式使用它们。对于直接使用派生对象时会发生什么,没有真正的保证,也没有必要保证。

因此,从这个意义上说,允许派生对象更改访问说明符,然后根据静态而非动态类型解析访问说明符,至少是一种与多态契约概念一致的选择.

综上所述,更改派生对象中的访问说明符无论如何都不是一个好主意。没有任何好处,因为它很容易变通,所以封装的好处为零,它只是暴露了这种奇怪的边缘情况。

在设计方面我并不是从根本上反对多重继承。然而,99% 的情况下,您最好避免使用钻石。同样是私有继承,几乎没有什么用处,枚举起来也比较简单:

  1. 针对空基class优化。
  2. 如果其他人编写了一个 class 具有虚函数的自定义技术,而您需要此 class 来实现您的。我说 "someone else" 是因为这种设计在更现代的 C++ 中很难证明是合理的,现在更容易传递 functions/lambda/std::function.

它是查找规则的衍生物,其中有一些与虚拟继承相关的要点。这个想法是虚拟继承不应该在名称查找期间引起歧义。所以在每个场景中都找到了不同的 getX() 声明。由于访问说明符仅在名称查找后检查(并且不影响名称解析),因此您可以遇到这样的障碍。

  • r2.getX(); 的情况下,查找从 IReadableData 的上下文开始。由于立即找到声明,并且 IReadableData 没有基础,这就是查找解析的内容。它是 IReadableData 的 public 成员,因此您可以命名和调用它。之后就是动态调度机制负责调用支配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]部分在标准中描述的过程。我不想在这方面引用标准,因为我觉得用简单的英语解释发生的事情并没有多大用处。但您可以按照 link 阅读完整规范。