C++私有虚继承问题

19
在下面的代码中,似乎类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继承与A中的私有构造函数以及通过友元授予B访问权限来封闭继承的实现。如果仅使用私有虚拟继承就足够了,所有这些复杂性都是不必要的。 - David Rodríguez - dribeas
如果只使用私有虚拟继承,所有的复杂性都是不必要的。在这里没有人声称密封习惯用法可以在没有私有构造函数的情况下工作。在密封习惯用法中,私有继承是不需要的,但它需要使使用该习惯用法成为实现细节。 - curiousguy
3个回答

14
根据C++核心问题 #7,带有虚拟私有基类的类不能被派生。这是编译器中的一个错误。

除了g++和como没有对核心问题中的示例提出异议之外。这足以让我怀疑我的答案,但我想要一个比旧问题更近期的参考,因为如果规则改变了,旧问题很可能没有更新。 - AProgrammer
事实上,这个问题已经关闭,意味着它不再是一个问题。 - Armen Tsirunyan
“带有虚拟私有基类的类不能被派生”是错误的。 - curiousguy
@curiousguy,请查看答案中的链接。这是C++委员会的官方“漏洞跟踪器”。 - Kirill V. Lyadvinsky
这个 bug 跟踪器包含一些毫无意义的内容,因为没有人关心它们是否会被纠正。在这个问题上,它甚至比 C++ 标准还不相关。关于虚继承的含义,存在普遍共识。 - curiousguy
2
+1 参考相关文档。-1 因为文档有误。 - ndkrempel

6

对于第二个问题,可能是因为您没有使其隐式定义。如果构造函数仅被隐式声明,则不会出现错误。例如:

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命名(包括没有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/6关于C的析构函数的说明:
所有析构函数都像使用限定名一样调用,即忽略任何可能存在于更派生类中的虚拟覆盖析构函数。
这可以有两种解释方式:
调用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();

1
"_这取决于编译器使用的名称。_”??? 没有使用名称,也没有名称查找问题。构造函数被调用。最好是可访问的。" - curiousguy
5
由于您的评论似乎被某个人反对了再次,我想为您上一条评论的第一部分提供C++11的11p4,即“访问控制统一应用于所有名称,无论这些名称是从声明还是表达式中引用的。”顺便说一句,“名称”的引入始终是一个声明。因此,“使用的声明”等同于“使用的名称”(“声明”的含义有两层。一层是指语法结构,另一层是指名称的引入)。 - Johannes Schaub - litb
6
许多事情都是不言自明的。我已经与规范一起工作多年,如果我不了解它的工作原理,那么它就不是不言自明的(如果你不同意,那么请提出你不同意的地方和原因,而不仅仅是空口说白话,比如“没有名称查找问题”、“许多事情都是不言自明的”。很抱歉,你的评论看起来像是一个自以为比别人聪明的人在恶意挑衅)。 - Johannes Schaub - litb
3
由于C++语言是通过其规范定义的,因此一个人永远不应该“忘记规范”或者基于“直觉”去实现细节。事实上,规范就是事实。 - Thomas Eding
2
请在聊天中进行讨论。谢谢! - Johannes Schaub - litb
显示剩余9条评论

2

虚基类总是从最派生的类(这里是C)初始化。编译器必须检查构造函数是否可访问(例如,使用g++ 3.4会出现错误)。

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

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

虽然您的描述暗示没有基类(while your description implies there is none),但实际上,基类A是私有或公有并不重要(要颠覆很容易:class C: public B, private virtual A)。

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

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


你应该说你只回答了第二个问题,而不是第一个。 - ndkrempel

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