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