【问题标题】:C++ private virtual inheritance problemC++私有虚拟继承问题
【发布时间】:2011-01-23 04:36:45
【问题描述】:

在下面的代码中,C 类似乎无法访问 A 的构造函数,这是由于虚拟继承所必需的。然而,代码仍然可以编译和运行。为什么会起作用?

class A {};
class B: private virtual A {};
class C: public B {};

int main() {
    C c;
    return 0;
}

此外,如果我从 A 中删除默认构造函数,例如

class A {
public:
    A(int) {}
};
class B: private virtual A {
public:
    B() : A(3) {}
};

然后

class C: public B {};

会(出乎意料地)编译,但是

class C: public B {
public:
    C() {}
};

无法按预期编译。

使用“g++ (GCC) 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)”编译的代码,但已验证与其他编译器的行为相同。

【问题讨论】:

  • 使用 g++ 4.4 可以编译。虽然我无法找到权威参考,但我相信它应该编译。派生最多的类C 可以构造A 类型的子对象。请注意,有一些实现基于private virtual 继承togetherA 中的私有构造函数的组合以及通过友谊授予B 的访问权限来密封继承。如果只使用私有虚拟继承就足够了,那么所有的复杂性都是不必要的。
  • @DavidRodríguez-dribeas "如果只使用私有虚拟继承就足够了,那么所有的复杂性都是不必要的。" 这里没有人声称密封习语在没有私有 ctor 的情况下也有效。在密封习语中,不需要私有继承,但需要将习语的使用作为实现细节。

标签: c++ inheritance encapsulation virtual-inheritance private-inheritance


【解决方案1】:

根据C++ Core Issue #7 类不能派生出具有虚拟私有基础的类。这是编译器中的一个错误。

【讨论】:

  • 除了 g++ 和 como 没有抱怨核心问题中的示例。这足以让我怀疑我的答案,但我想要一个更新的参考资料,而不是如果规则发生变化很可能不会更新的旧问题之一。
  • 其实这个issue已经关闭了,意思是不是issue。
  • "具有虚拟私有基的类不能派生自"错误。
  • @curiousguy,检查答案中的链接。这是 C++ 委员会的官方“错误跟踪器”。
  • +1 用于引用相关文档。 -1 因为文档错误。
【解决方案2】:

对于第二个问题,可能是因为你没有让它被隐式定义。如果构造函数只是隐式声明,则没有错误。示例:

struct A { A(int); };
struct B : A { };
// goes fine up to here

// not anymore: default constructor now is implicitly defined 
// (because it's used)
B b;

对于您的第一个问题 - 这取决于编译器使用的名称。我不知道标准规定了什么,但是这个代码是正确的,因为外部类名(而不是继承的类名)是可访问的:

class A {};
class B: private virtual A {};
class C: public B { C(): ::A() { } }; // don't use B::A

也许在这一点上标准没有被详细说明。我们得看看。


代码似乎没有任何问题。此外,有迹象表明该代码是有效的。 (虚拟)基类子对象是默认初始化的——没有文本暗示类名的名称查找在C 的范围内。标准是这样说的:

12.6.2/8 (C++0x)

如果给定的非静态数据成员或基类不是由 mem-initializer-id 命名的(包括 case 其中没有 mem-initializer-list 因为构造函数没有 ctor-initializer)并且实体不是抽象类的虚拟基类

[...] 否则,实体默认初始化

C++03 也有类似的文本(你的文本不太清楚——它只是说它的默认构造函数在一个地方被调用,而在另一个地方它依赖于类是否是 POD)。对于默认初始化子对象的编译器,它只需调用其默认构造函数 - 无需首先查找基类的名称(它已经知道考虑的是什么基类)。

考虑一下这段代码肯定是有效的,但如果这样做会失败(参见 C++0x 中的12.6.2/4

struct A { };
struct B : virtual A { };
struct C : B, A { };
C c;

如果编译器的默认构造函数只是在C 中查找类名A,那么关于要初始化的子对象的查找结果就会不明确,因为非虚拟A 和找到虚拟A 的类名。如果您的代码旨在格式错误,我会说标准当然需要澄清。


对于构造函数,注意12.4/6C 的析构函数的描述:

所有析构函数都被调用,就好像它们是用限定名称引用的一样,也就是说,忽略更多派生类中任何可能的虚拟覆盖析构函数。

这可以用两种方式解释:

  • 调用 A::~A()
  • 调用 ::A::~A()

在我看来,这里的标准不太清楚。第二种方法将使其有效(通过3.4.3/6,C++0x,因为两个类名A 在全局范围内查找),而第一种方法将使其无效(因为A 都会找到继承的类名)。它还取决于搜索从哪个子对象开始(我相信我们将不得不使用虚拟基类的子对象作为起点)。如果是这样的

virtual_base -> A::~A();

然后我们将直接找到虚拟基类的名称作为公共名称,因为我们不必经过派生类的作用域并找到名称不可访问。同样,推理是相似的。考虑:

struct A { };
struct B : A { };
struct C : B, A {
} c;

如果析构函数只是调用this->A::~A(),则此调用将无效,因为A 作为继承类名的查找结果不明确(您不能引用直接基类的任何非静态成员函数范围 C 中的类对象,请参阅 10.1/3,C++03)。它必须唯一地标识所涉及的类名,并且必须以类的子对象引用开头,例如a_subobject->::A::~A();

【讨论】:

  • "这取决于编译器使用什么名称。" ???没有“使用”名称,也没有名称查找问题。 构造函数被调用。它们最好是可访问的。
  • @curious 因为显然有人刚刚阅读了您的评论,所以这个答案再次被否决了,我想在第一部分为您提供 C++11 的 11p4您的最后一条评论:“访问控制统一应用于所有名称,无论名称是从声明还是表达式中引用。”。 FWIW,名称的引入始终是一种声明。所以“使用的声明”等价于“使用的名称”(“声明”的含义有两个,一个是指句法结构,一个是指名称的引入)。
  • “很多事情是不言而喻的。” 我多年来一直在使用规范,如果我不明白它是如何工作的,那么它确实是 not 不言而喻(如果您不同意,请提出您不同意的内容和原因,而不仅仅是像“没有名称查找问题”/“很多事情不言而喻”这样的简单陈述。对不起,你的 cmets 看起来像是在欺骗自认为比别人聪明的人。
  • @curiousguy:由于 C++ 语言是由其规范定义的,因此永远不要“忘记规范”,也永远不要依赖“直觉”来获取实现细节。事实是规范的事实。
  • 请在聊天中进行讨论。谢谢!
【解决方案3】:

虚拟基类总是从最派生类(这里是 C)初始化。编译器必须检查构造函数是否可访问(即我在 g++ 3.4 for 中遇到错误

class A { public: A(int) {} };
class B: private virtual A {public: B() : A(0) {} };
class C: public B {};

int main() {
    C c;
    return 0;
}

虽然您的描述暗示没有),但作为基础,A 是否是私有的这一事实并不重要(颠覆很容易:class C: public B, private virtual A)。

从最派生类调用虚拟基类构造函数的原因是它需要在任何将它们作为基类的类之前构造。

编辑:Kirill 提到了一个旧的核心问题,这与我的阅读和最近编译器的行为不一致。我会尝试以一种或另一种方式获得标准参考,但这可能需要时间。

【讨论】:

  • 你应该说你只是在解决第二个问题而不是第一个问题。
猜你喜欢
  • 1970-01-01
  • 2017-12-09
  • 2012-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-26
  • 2011-01-08
相关资源
最近更新 更多