虚拟继承中的主导地位

发布于 2024-12-01 12:55:44 字数 216 浏览 3 评论 0原文

C++98/C++03 标准和 C++0x 未来标准的具体规则是什么 虚拟继承中的主导地位

我不只是要求特定的段落,尽管我也要求特定的段落(我猜是在第 10 节的某个地方)。

我还想问标准语的后果,标准语解释得很清楚。

What are the C++98/C++03 standards' and the C++0x future standard's exact rules for dominance in virtual inheritance?

I'm not asking for just the specific paragraphs, although I'm asking also for that (somewhere in section 10, I'd guess).

I'm asking also for the consequences of the standardese, the standardese explained, clearly.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

∞琼窗梦回ˉ 2024-12-08 12:55:44

我认为这就是您正在寻找的语言。在 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::fD::f 添加到可以考虑的名称集中。 D::f 不会隐藏任何内容,因为 D 没有基类。但是,B::f 确实隐藏了 D::f,因此即使可以从 A< 访问 D::f /code> 没有看到 B::f,它被视为隐藏并从可以命名为 f 的对象集中删除。由于只剩下 B::f ,因此这就是被调用的那个。 ISO 规范提到 (§10.2/7)

当使用虚拟基类时,可以沿着通过子对象的路径到达隐藏声明
不通过隐藏声明的格子。这并不是含糊之处。 [...]

我认为这是因为上述规则。

在 C++11 中(根据草案规范 N3242),规则的拼写比以前更加明确,并且给出了实际的算法来计算名称的含义。这是语言,一步一步。

我们从 §10.2/3 开始:

C 中 f 的查找集称为 S(f, C),由两个组件集组成:声明集,一组成员
名为f;以及子对象集,这是一组子对象,其中找到了这些成员的声明(可能包括 using 声明)。在声明集中,使用声明被替换为它们的成员。
指定,类型声明(包括注入类名)被它们指定的类型替换。
S(f,C)计算如下:

在本上下文中,C 指的是查找发生的范围。换句话说,集合 S(f, C) 的意思是“当我尝试在类作用域 Cf 时,可见的声明是什么”代码>?”为了回答这个问题,规范定义了一个算法来确定这个问题。第一步如下:(§10.2/4)

如果 C 包含名称 f 的声明,则声明集包含在中声明的 f 的每个声明
C 满足查找发生的语言结构的要求。 [...] 如果生成的声明集不为空,则子对象集包含 C
本身,计算完成。

换句话说,如果类本身声明了名为 f 的东西,那么声明集就是在该类中定义的名为 f 的东西的集合(或使用using 声明)。但是,如果我们找不到名为 f 的任何内容,或者名为 f 的所有内容的排序错误(例如,当我们需要类型时,却是函数声明),然后我们继续下一步:(§10.2/5)

否则(即,C 不包含 f 的声明或结果声明集为空),S(f, C) 最初为空。如果C有基类,计算每个直接基类子对象Bi中f的查找集,并依次合并每个这样的查找集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 的值>fC 开始。让我们看看会发生什么。当计算 Bf 的含义时,我们看到定义了 B::f,因此我们停止查找。在B中查找f的值是集合(B::f, B}。了解fC中的含义,我们查看C,发现它没有定义f,所以我们再次从 D 中递归查找值。 D 产生 {D::f, D},当我们将所有内容合并在一起时,我们发现规则 1 的后半部分适用(因为子对象集中的每个对象都是 D 的基数,这是毫无疑问的事实),因此 C 的最终值由 {D::fD::f 给出代码>,<代码>D}

。需要将 BC 的值合并在一起,这会尝试合并 {D::f, D}。和 {B::f, B} ,这就是有趣的地方。假设我们按这个顺序合并。 code>, D} 和空集产生{D::f, D}。当我们现在合并到 {B::f, B} 时。 ,然后因为 DB 的基数,根据规则一的后半部分,我们覆盖旧的集合并最终得到 {B::f< /代码>,<代码>B}。因此,f 的查找是Bf 的版本。

另一方面,如果我们以相反的顺序合并,我们从 {B::f, B} 开始,并尝试在 {D:: fD}。但由于 DB 的基数,因此我们忽略它,留下 {B::f, B }。我们得到了相同的结果。很酷吧?我很惊讶这效果这么好!

现在你明白了——旧规则确实很简单,而新规则则复杂得难以置信,但无论如何都能解决。

希望这有帮助!

I think that this is the language you're looking for. In the C++03 ISO spec, in §10.2/2, we have the following:

The following steps define the result of name lookup in a class scope, C. First, every declaration for the
name in the class and in each of its base class sub-objects is considered. A member name f in one sub-object B hides a member name f in a sub-object A if A is a base class sub-object of B. Any declarations
that are so hidden are eliminated from consideration. Each of these declarations that was introduced by a
using-declaration is considered to be from each sub-object of C that is of the type containing the declaration designated by the using-declaration. If the resulting set of declarations are not all from sub-objects
of the same type, or the set has a nonstatic member and includes members from distinct sub-objects, there is
an ambiguity and the program is ill-formed. Otherwise that set is the result of the lookup.

At a high level, this means that when you try looking up a name, it looks in all of the base classes and the class itself to find declarations for that name. You then go class-by-class and if one of those base objects has something with that name, it hides all of the names introduced in any of that object's base classes.

An important detail here is this line:

Any declarations
that are so hidden are eliminated from consideration.

Importantly, this says that if something is hidden by anything, it's considered hidden and removed. So, for example, if I do this:

                            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
                           }
                       };

On the indicated line, the call to f() is resolved as follows. First, we add B::f and D::f to the set of names that could be considered. D::f doesn't hide anything because D has no base classes. However, B::f does hide D::f, so even though D::f can be reached from A without seeing B::f, it's considered hidden and removed from the set of objects that could be named f. Since only B::f remains, that's the one that's called. The ISO spec mentions (§10.2/7) that

When virtual base classes are used, a hidden declaration can be reached along a path through the sub-object
lattice that does not pass through the hiding declaration. This is not an ambiguity. [...]

I think that this is because of the above rule.

In C++11 (according to draft spec N3242), the rules are spelled out much more explicitly than before and an actual algorithm is given to compute what name is meant. Here is the language, step-by-step.

We begin with §10.2/3:

The lookup set for f in C, called S(f, C), consists of two component sets: the declaration set, a set of members
named f; and the subobject set, a set of subobjects where declarations of these members (possibly including using-declarations) were found. In the declaration set, using-declarations are replaced by the members they
designate, and type declarations (including injected-class-names) are replaced by the types they designate.
S(f, C) is calculated as follows:

In this context, C refers to the scope in which the lookup occurs. In otherwords, the set S(f, C) means "what are the declarations that are visible when I try to look up f in class scope C?" To answer this, the spec defines an algorithm to determine this. The first step is as follows: (§10.2/4)

If C contains a declaration of the name f, the declaration set contains every declaration of f declared in
C that satisfies the requirements of the language construct in which the lookup occurs. [...] If the resulting declaration set is not empty, the subobject set contains C
itself, and calculation is complete.

In other words, if the class itself has something called f declared in it, then the declaration set is just the set of things named f defined in that class (or imported with a using declaration). But, if we can't find anything named f, or if everything named f is of the wrong sort (for example, a function declaration when we wanted a type), then we go on to the next step: (§10.2/5)

Otherwise (i.e., C does not contain a declaration of f or the resulting declaration set is empty), S(f, C) is initially empty. If C has base classes, calculate the lookup set for f in each direct base class subobject Bi, and merge each such lookup set S(f, Bi) in turn into S(f, C).

In other words, we're going to look at the base classes, compute what the name could refer to in those base classes, then merge everything together. The actual way that you do the merge is specified in the next step. This is really tricky (it has three parts to it), so here's the blow-by-blow. Here's the original wording: (§10.2/6)

The following steps define the result of merging lookup set S(f, Bi) into the intermediate S(f, C):

  • If each of the subobject members of S(f, Bi) is a base class subobject of at least one of the subobject
    members of S(f, C), or if S(f, Bi) is empty, S(f, C) is unchanged and the merge is complete. Conversely, if each of the subobject members of S(f, C) is a base class subobject of at least one of the
    subobject members of S(f, Bi), or if S(f, C) is empty, the new S(f, C) is a copy of S(f, Bi ).

  • Otherwise, if the declaration sets of S(f, Bi) and S(f, C) differ, the merge is ambiguous: the new
    S(f, C) is a lookup set with an invalid declaration set and the union of the subobject sets. In subsequent
    merges, an invalid declaration set is considered different from any other.

  • Otherwise, the new S(f, C) is a lookup set with the shared set of declarations and the union of the
    subobject sets.

Okay, let's piece this apart one at a time. The first rule here has two parts. The first part says that if you're trying to merge an empty set of declarations into the overall set, you don't do anything at all. That makes sense. It also says that if you're trying to merge something in that's a base class of everything already merged so far, then you don't do anything at all. This is important, because it means that if you've hidden something, you don't want to accidentally reintroduce it by merging it back in.

The second part of the first rule says that if the thing you're merging in is derived from everything that's been merged so far, you replace the set you've computed up to this point with the data you've computed for the derived type. This essentially says that if you've merged together a lot of classes that seem unconnected and then merge in a class that unifies all of them, throw out the old data and just use the data for that derived type, which you've already computed.

Now let's go to that second rule. This took me a while to understand, so I may have this wrong, but I think that it's saying that if you conduct the lookup in two different base classes and get back different things, then the name is ambiguous and you should report that something is wrong if you were to try to look up the name at this point.

The last rule says that if we aren't in either of these special cases, nothing is wrong and you should just combine them.

Phew... that was tough! Let's see what happens when we trace this out for the diamond inheritance above. We want to look up the name f starting in A. Since A doesn't define f, we compute the values of looking up f starting in B and f starting in C. Let's see what happens. When computing the value of what f means in B, we see that B::f is defined, and so we stop looking. The value of looking up f in B is the set (B::f, B}. To look up what f means in C, we look in C and see that it does not define f, so we again recursively look up the value from D. Doing the lookup in D yields {D::f, D}, and when we merge everything together we find that the second half of rule 1 applies (since it's vacuously true that every object in the subobject set is a base of D), so the final value for C is given by {D::f, D}.

Finally, we need to merge together the values for B and C. This tries merging {D::f, D} and {B::f, B}. This is where it gets fun. Let's suppose we merge in this order. Merging {D::f, D} and the empty set produces {D::f, D}. When we now merge in {B::f, B}, then because D is a base of B, by the second half of rule one, we override our old set and end up with {B::f, B}. Consequently, the lookup of f is the version of f in B.

If, on the other hand, we merge in the opposite order, we start with {B::f, B} and try merging in {D::f, D}. But since D is a base of B, we just ignore it, leaving {B::f, B}. We've arrived at the same result. Pretty cool, huh? I'm amazed that this works out so well!

So there you have it - the old rules are really (ish) straightforward, and the new rules are impossibly complex but somehow manage to work out anyway.

Hope this helps!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文