C++98/C++03 标准和 C++0x 未来标准在虚拟继承中占主导地位的确切规则是什么?
我不只是要求特定的段落,尽管我也在要求(我猜在第 10 节的某个地方)。
我也在询问标准语言的后果,标准语言解释得很清楚。
C++98/C++03 标准和 C++0x 未来标准在虚拟继承中占主导地位的确切规则是什么?
我不只是要求特定的段落,尽管我也在要求(我猜在第 10 节的某个地方)。
我也在询问标准语言的后果,标准语言解释得很清楚。
我认为这是您正在寻找的语言。在 C++03 ISO 规范的 §10.2/2 中,我们有以下内容:
以下步骤定义在类范围 C 中名称查找的结果。首先,考虑类中及其每个基类子对象中名称的每个声明。如果 A 是 B 的基类子对象,则一个子对象 B 中的成员名称 f 隐藏子对象 A 中的成员名称 f。如此隐藏的任何声明都将不予考虑。由 using 声明引入的这些声明中的每一个都被认为来自 C 的每个子对象,这些子对象的类型包含由 using 声明指定的声明。如果声明的结果集并非全部来自同一类型的子对象,或者该集具有非静态成员并包含来自不同子对象的成员,则存在歧义并且程序格式错误。否则,该集合是查找的结果。
在高层次上,这意味着当您尝试查找名称时,它会在所有基类和类本身中查找该名称的声明。然后,您逐类进行,如果其中一个基对象具有该名称,它会隐藏该对象的任何基类中引入的所有名称。
这里的一个重要细节是这一行:
任何如此隐藏的声明都将不予考虑。
重要的是,这表示如果某些东西被任何东西隐藏,它就会被认为是隐藏和删除的。因此,例如,如果我这样做:
class D {
public:
void f();
}
class B: virtual public D { class C: virtual public D {
public: public:
void f(); /* empty */
}; };
class A: public B, public C {
public:
void doSomething() {
f(); // <--- This line
}
};
在指示的行上,调用f()
如下解决。首先,我们将B::f
和添加D::f
到可以考虑的名称集合中。 D::f
没有隐藏任何东西,因为D
没有基类。但是,B::f
确实 hide D::f
,因此即使D::f
可以在A
没有看到的情况下到达B::f
,它也被视为隐藏并从可以命名的对象集中删除f
。既然只剩下B::f
,那就是所谓的那个。ISO 规范提到 (§10.2/7)
当使用虚拟基类时,可以沿着通过子对象晶格的路径到达隐藏声明,该路径不通过隐藏声明。这不是模棱两可的。[...]
我认为这是因为上述规则。
在 C++11 中(根据草案规范 N3242),规则的说明比以前更加明确,并且给出了一个实际的算法来计算名称的含义。这是语言,一步一步。
我们从 §10.2/3 开始:
C 中 f 的查找集,称为 S(f, C),由两个组件集组成:声明集,一组名为 f 的成员;和subobject set,一组子对象,其中找到了这些成员的声明(可能包括 using-declarations)。在声明集中,使用声明被它们指定的成员替换,类型声明(包括注入类名)被它们指定的类型替换。S(f, C) 计算如下:
在此上下文中,C
指的是查找发生的范围。换句话说,该集合的意思是“当我尝试在类范围内S(f, C)
查找时可见的声明是什么?” 为了回答这个问题,规范定义了一个算法来确定这一点。第一步如下:(§10.2/4)f
C
如果 C 包含名称为 f 的声明,则声明集包含在 C 中声明的每个满足查找发生的语言构造要求的 f 声明。[...] 如果结果声明集不为空,则子对象集包含 C 本身,计算完成。
换句话说,如果类本身有一些叫做f
声明的东西,那么声明集就是f
在那个类中定义的命名的东西的集合(或通过声明导入using
)。但是,如果我们找不到任何命名的东西f
,或者如果命名的所有东西f
都是错误的类型(例如,当我们想要一个类型时的函数声明),那么我们继续下一步:(§10.2/5)
否则(即,C 不包含 f 的声明或结果声明集为空),S(f, C) 最初为空。如果 C 有基类,则计算每个直接基类子对象 B i中 f 的查找集,并将每个这样的查找集 S(f, B i ) 依次合并为 S(f, C)。
换句话说,我们将查看基类,计算名称在这些基类中可以引用的内容,然后将所有内容合并在一起。执行合并的实际方式将在下一步中指定。这真的很棘手(它包含三个部分),所以这里是逐个介绍。这是原始措辞:(§10.2/6)
以下步骤定义了将查找集 S(f, B i ) 合并到中间 S(f, C) 中的结果:
如果 S(f, B i ) 的每个子对象成员是 S(f, C) 的至少一个子对象成员的基类子对象,或者如果 S(f, B i ) 为空,则 S(f , C) 不变,合并完成。相反,如果 S(f, C) 的每个子对象成员是 S(f, B i )的至少一个子对象成员的基类子对象,或者如果 S(f, C) 为空,则新的S(f, C) 是 S(f, Bi ) 的副本。
否则,如果 S(f, B i ) 和 S(f, C) 的声明集不同,则合并是不明确的:新的 S(f, C) 是具有无效声明集的查找集,并且子对象集。在随后的合并中,无效的声明集被认为与其他任何不同。
否则,新的 S(f, C) 是具有共享声明集和子对象集并集的查找集。
好吧,让我们一次一个地拆开这个。这里的第一条规则有两个部分。第一部分说如果你试图将一组空的声明合并到整个集合中,你根本什么都不做。这就说得通了。它还说,如果您尝试将某些东西合并到迄今为止已经合并的所有内容的基类中,那么您根本什么都不做。这很重要,因为这意味着如果您隐藏了某些内容,您不想通过将其重新合并来意外地重新引入它。
第一条规则的第二部分说,如果您要合并的东西是从迄今为止已合并的所有内容中派生的,则将您到目前为止计算的集合替换为您为派生类型计算的数据. 这实质上是说,如果您将许多看似未连接的类合并在一起,然后合并到一个统一所有这些类的类中,则丢弃旧数据并仅使用您已经计算过的派生类型的数据.
现在让我们来看第二条规则。这花了我一段时间才理解,所以我可能有这个错误,但我认为它的意思是,如果你在两个不同的基类中进行查找并返回不同的东西,那么名称是不明确的,你应该报告某事是如果您此时尝试查找名称,则错误。
最后一条规则说,如果我们不在这两种特殊情况下,没有什么是错的,你应该把它们结合起来。
呼……太难了!让我们看看当我们追踪上面的钻石继承时会发生什么。我们要查找以f
开头的名称A
。由于A
没有定义f
,我们计算查找f
起始于B
和f
起始于的值C
。让我们看看发生了什么。在计算 what f
mean in的值时B
,我们看到它B::f
已定义,因此我们停止查找。查找f
in的值B
是集合 ( B::f
, B
}。要查找f
in 的含义C
,我们查看C
它并发现它没有定义f
,因此我们再次递归查找 from 的值D
。进行查找D
产生 {D::f
, D
},当我们将所有内容合并在一起时,我们发现规则 1 的后半部分适用(因为子对象集中的每个对象都是 的基是空洞的D
),所以最终的值C
由 { D::f
, D
} 给出。
最后,我们需要将 和 的值合并B
在一起C
。这尝试合并 { D::f
, D
} 和 { B::f
, B
}。这就是它变得有趣的地方。假设我们按此顺序合并。合并 { D::f
, D
} 和空集产生 { D::f
, D
}。当我们现在合并 { B::f
, B
} 时,因为D
是 的基数B
,所以在规则一的后半部分,我们会覆盖旧集合并以 { B::f
, B
} 结尾。因此,查找的f
是f
in的版本B
。
另一方面,如果我们以相反的顺序合并,我们从 { B::f
, B
} 开始并尝试在 { D::f
, D
} 中合并。但由于D
是 的基数B
,我们就忽略它,留下 { B::f
, B
}。我们得到了相同的结果。很酷吧?我很惊讶这效果如此之好!
所以你有它 - 旧规则真的(ish)简单明了,而新规则非常复杂,但无论如何都能设法解决。
希望这可以帮助!