Dominance in virtual inheritance
Asked Answered
E

1

13

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.

Elastance answered 26/8, 2011 at 21:44 Comment(5)
What do you mean by "dominance" with regard to virtual inheritance?Coset
@Nicol: the c++98 standard's index refers to virtual inheritance and page 167. that's all the standard says directly (and as far as i know the index is non-normative). explaining dominance would take more than this comment, so, just google it, or wikipedia -- if you don't know about it, you probably can't answer this question anyway. cheers,Elastance
@Nicol: thanks for asking, i added link to the question.Elastance
Is standardeese related to cheese?Shoat
@Tomalak: it's just my way of spelling standardese.Elastance
C
25

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!

Clownery answered 27/8, 2011 at 0:53 Comment(3)
@Alf P. Steinbach- I just updated this with the rules for C++11 and man is it a doozy. Hope this helps explain things(?)!Clownery
+1 for wading into this. I think I understood the C++98 part now. OK. Surprisingly simple. However, my eyes glazed over when I started reading the C++11 part... Cheers,Elastance
+1 just for the effort that must have gone into this answer o_OEquestrian

© 2022 - 2024 — McMap. All rights reserved.