I think the paragraph here explains this behavior:
Either way, when examining the bases from which the class is derived, the following rules, sometime referred to as dominance in virtual inheritance, are followed:
A lookup set is constructed, which consists of the declarations and
the subobjects in which these declarations were found.
Using-declarations are replaced by the members they represent and type
declarations, including injected-class-names are replaced by the types
they represent. If C is the class in whose scope the name was used, C
is examined first. If the list of declarations in C is empty, lookup
set is built for each of its direct bases Bi (recursively applying
these rules if Bi has its own bases). Once built, the lookup sets for
the direct bases are merged into the lookup set in C as follows
- if the set of declarations in Bi is empty, it is discarded
- if the lookup set of C built so far is empty, it is replaced by the lookup set of Bi
- if every subobject in the lookup set of Bi is a base of at least one of
the subobjects already added to the lookup set of C, the lookup set of
Bi is discarded.
- if every subobject already added to the lookup set of
C is a base of at least one subobject in the lookup set of Bi, then
the lookup set of C is discarded and replaced with the lookup set of
Bi
- otherwise, if the declaration sets in Bi and in C are different,
the result is an ambiguous merge: the new lookup set of C has an
invalid declaration and a union of the subobjects ealier merged into C
and introduced from Bi. This invalid lookup set may not be an error if
it is discarded later.
- otherwise, the new lookup set of C has the
shared declaration sets and the union of the subobjects ealier merged
into C and introduced from Bi
The example helps illustrate the logic here:
struct X { void f(); };
struct B1: virtual X { void f(); };
struct B2: virtual X {};
struct D : B1, B2 {
void foo() {
X::f(); // OK, calls X::f (qualified lookup)
f(); // OK, calls B1::f (unqualified lookup)
// C++11 rules: lookup set for f in D finds nothing, proceeds to bases
// lookup set for f in B1 finds B1::f, and is completed
// merge replaces the empty set, now lookup set for f in C has B1::f in B1
// lookup set for f in B2 finds nothing, proceeds to bases
// lookup for f in X finds X::f
// merge replaces the empty set, now lookup set for f in B2 has X::f in X
// merge into C finds that every subobject (X) in the lookup set in B2 is a base
// of every subobject (B1) already merged, so the B2 set is discareded
// C is left with just B1::f found in B1
// (if struct D : B2, B1 was used, then the last merge would *replace* C's
// so far merged X::f in X because every subobject already added to C (that is X)
// would be a base of at least one subobject in the new set (B1), the end
// result would be the same: lookup set in C holds just B1::f found in B1)
}
};
TL;DR: Because e.m = 303;
is an unqualified lookup, the compiler will recursively look up the inheritance tree for matching declarations. In this case I think it would first find A::m
, but would replace this with D::m
after seeing that D
has A
as an indirect base class. So e.m
ends up resolving to e.D::m
.