7

Recently I stumbled across the Visual C++ compiler conformance mode switch in VS 2017. I read this explanation which gave the following of how the switch could prohibit non-conforming code from compiling

template<typename T>
struct B {
    int f();
};

template<typename T>
struct D : B<T> {
    int g();
};

template<typename T>
int D<T>::g() {
    return f();  // error: should be ‘this->f()’
}

In the definition of D::g, the symbol f is from the dependent base class B but standard C++ does not permit examining dependent base classes when looking for declarations that satisfy the use of f. That is an error in the source code that Visual C++ has long failed to diagnose.

OK, fine, I get it. Except for one thing. Why?

Why would the standard not permit examining a dependent base class for f()? What is the justification for this prohibition. Does the standard give one?

If B and D were both just regular, non-template structs, f() would properly be interpreted as a call to the base class (er... base struct) member function. So why is it this not done when they are templates?

(I'm sure there's a good reason but with my limited understanding at the moment it just seems like an annoyance. I did try to search on this and found at least one question regarding it but none with the "why" of it)

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
Joe
  • 5,394
  • 3
  • 23
  • 54

1 Answers1

7

Because templates might be specialized later. e.g.

template<typename T>
struct B {
    int f();
};

template<typename T>
struct D : B<T> {
    int g();
};

template<typename T>
int D<T>::g() {
    return f();  // the name f won't be looked up when not knowing the exact type of T
}

template<>
struct B<int> {
    // no function named f for B<int>
};

So standard C++ says that nondependent names are not looked up in dependent base classes.

Adding this-> makes the name dependent and dependent names can be looked up only at the time of instantiation, and at that time the exact base specialization that must be explored will be known.

Also refer to Two Phase Lookup.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • OK, you defined B without f(). That means that any attempt to define D is invalid because there is no f() to call. So why not leave it at that? Let the compilation fail in that case, as it should. (Or through SFINAE, be removed from a list of candidates)? D would not compile. But if you specialized B for float (B) and did implement f() in that case, D would compile. It seems to me that name already *is* dependent. This is inheritance. struct D *is* a B. Inheritance - an "is-a" relationship. I know I am dense but I still don't get it. – Joe Feb 27 '18 at 17:03
  • Ah, two phase lookup. I knew it had to be something obvious. Duh. Cancel that. I think I get it now. That's a topic I've always sort of ignored until now – Joe Feb 27 '18 at 17:05
  • 1
    @Joe Yes, two phase lookup. In short, it fails at the 1st phase, and adding `this->` defer the lookup to the 2nd phase. – songyuanyao Feb 27 '18 at 17:09
  • 2
    @Joe - The lookup can be complicated by the existence of a non-member `f()`. Should that be used for `B`, or should the missing member function be an error? If you have to say `this->f()` the compiler knows. – Bo Persson Feb 27 '18 at 19:48
  • Bo, my argument was that it should not be complicated as the template is clearly listed as deriving from one that exposes a member function f(), just as it would not be complicated by simple, non-template structs. Of course, as songyuanyao pointed out, that was before I had considered two-phase lookup. I guess it was easy for me to get away with not knowing this before. The Visual Studio compiler is making some big (and long overdue) conformance improvements right now. Two-phase lookup is clearly one of them. Enabling conformance mode is a real eye-opener. – Joe Feb 27 '18 at 20:52