A quirk of qualified member lookup

Yesterday on the cpplang Slack, someone posted this bizarre MSVC behavior:

namespace A { int i; }
namespace B { struct S {}; }

void test() {
    B::S bs;
    bs.A::i = 42;
}

MSVC accepts this invalid C++ code, and — even weirder — seems to enter a mode in which no matter what you do with bs.A::i, it doesn’t generate any actual machine code. Okay, so another day, another MSVC parser bug, right?

But then Momchil Velikov pointed to some “weird-looking text” in [basic.lookup.classref]/4:

If the id-expression in a class member access is a qualified-id of the form class-name-or-namespace-name::... the class-name-or-namespace-name following the . or -> operator is first looked up in the class of the object expression and the name, if found, is used. Otherwise it is looked up in the context of the entire postfix-expression.

This wording doesn’t explain MSVC’s wacky parser behavior, but it is news to me. It deals with cases like the following (Godbolt):

struct A { void foo(); };
struct B { void foo(); };
struct C : A, B { void foo(); };
struct D : A, B { using T = B; void foo(); };

void one(C *c) {
    using T = A;
    c->T::foo();  // A::foo
}

void two(D *d) {
    using T = A;
    d->T::foo();  // B::foo (!!)
}

That’s right — d->T::foo() refers to the T that is a member of D — that is, the T that means B — completely ignoring the local meaning of T as an alias for A! Contrariwise, in c->T::foo(), the name T is looked up in the context of class C and is not found; therefore it is looked up a second time in the local scope and we get A in that case.

This gets even more confusing when you bring templates into it.

template<class T>
void three(T *t) {
    t->T::foo();
}

template void three<C>(C*);  // calls C::foo
template void three<D>(D*);  // calls B::foo

See, the same logic applies in this case: when t is of type D, t->T::foo() refers to the T that is a member of D (that is, B). But when t is of type C, there is no such T and therefore T has its local meaning as a template type parameter (that is, C itself).

MSVC, Clang, and ICC all handle this as shown above. GCC alone dissents: if it sees t->T::foo and t’s type is template-dependent, it’ll always use the local T instead of looking it up in the scope of t. However, this only applies to template-dependent things!

template<class T>
void three(T *t) {
    t->T::foo();
}
template void three<D>(D*);  // GCC (wrongly) calls D::foo

template<class T>
void four(D *t) {
    t->T::foo();
}
template void four<D>(D*);  // GCC calls B::foo

CWG issue 1089 deals with this kind of weirdness.

Who benefits from this confusing double-lookup rule? The behavior of function two above seems utterly counterintuitive. It would be interesting to see compiler vendors emit a warning whenever this arcane rule is triggered; my guess is that no real codebase would see that warning, and eventually maybe C++ could get rid of the rule. (See also: Contra implicit declarations of struct types” (2018-05-16), “Field-testing Herb Sutter’s Modest Proposal to Fix ADL” (2018-08-13).)

Posted 2020-01-11