Dependent Names With A Little Encouragement | consteval

5 min read Original article ↗

Hey, fuckface! Think fast!

template<typename F> void invokeFoo(F& f) {
  f.foo();
}

struct Fooable {
    void foo() {}
};

int main() {
    Fooable c;
    invokeFoo(c);
}

This is normal C++ code—nothing particularly nefarious afoot. invokeFoo is a function that takes a hopefully-fooable argument and, well, calls its foo method. That’s fine and well, but what if we further assume that F::foo is a member template? We might be tempted to write something like this:

template<typename F, typename P> void invokeFoo(F& f) {
    f.foo<P>();
}

struct Fooable {
    template<typename P> void foo() {}
};

int main() {
    Fooable c;
    invokeFoo<Fooable, int>(c);
}

You might be surprised to know that this actually fails to compile. Here’s an excerpt of what GCC says:

temp.cc: In function ‘void invokeFoo(F&)’:
temp.cc:2:12: error: expected primary-expression before ‘>’ token
    2 |     f.foo<P>();
      |            ^
temp.cc:2:14: error: expected primary-expression before ‘)’ token
    2 |     f.foo<P>();
      |              ^

Hey, that’s a new one. A parse error? How? We didn’t even do anything particularly offensive here—or so one might think. Let me add some suggestive whitespace to imply a different parse:

template<typename F, typename P> void invokeFoo(F& f) {
    f.foo < P > (); // !
}

Does it make any god damn sense at all? No. But, you get the picture, I hope: that < could be a less-than comparison operator! That sounds dumb, right? I mean, if I write std::function<int()> f;, the compiler knows that I don’t mean to perform an ordering comparison as suggested by std::function < int() > f; (which would be valid for the right definitions of std::function and f!) because it looks up std::function, sees that it’s a template, and concludes that it should parse the stuff following it as a template parameter list.

So, what’s the difference here? Well, in invokeFoo, f has type F, which could be anything! As such, there’s really no way to determine what F::foo might be. It doesn’t have to be a template! It could just be a regular old data member, in which case the ordering comparison would be what we want—and, sure, in most cases, this sort of thing is generally meant to parse as a template parameter list, but you’d need an insane amount of look-ahead in the parser to be able to determine even some cases. Even then, it might not be enough. Here—allow me to muddy the waters with some well-formed C++:

template<typename F> void weird(F& f) {
    F::foo < int() > f; // N.B. `int()` evaluates to 0
}

struct HasStaticFoo {
    static int foo;
};
int HasStaticFoo::foo = 42;
bool operator>(bool b, const HasStaticFoo& f) { return true; }

int main() {
    HasStaticFoo f;
    weird(f);
}

Filthy, turbid, fetid water.

∗  ∗  ∗

Anyway, given all this, how do we signal to the compiler that we really mean for this to be interpreted as a template parameter list? Here’s what the great minds at C++ HQ came up with:

template<typename F, typename P> void invokeFoo(F& f) {
    f.template foo<P>();
}

Gulp. Hey, are you saying you could’ve come up with something better? Anyway, the language we use to refer to something like this is that f.foo is a dependent name. It can’t be looked up until we have a better idea of the shape of the type F (on which f.foo depends), so the compiler needs a little help in the meantime.

This problem has a similar analog for template types—one that you’re perhaps more likely to have seen before:

template<typename F> void weird() {
    F::Nested();
}

What are the semantics here? Does F::Nested refer to a static member Nested of F, or does it refer to a constructor of a nested class Nested declared inside F? It’s the difference between a type and a value, and either could be valid. There’s no way to distinguish before instantiation, so the compiler will interpret it as a function unless directed otherwise. If you want it to be interpreted as a nested class instead, you’ll need to give the compiler a little encouragement:

template<typename F> void weird() {
    typename F::Nested();
}

Moreover, if you want Nested to be read as a nested template class, the compiler demands even more encouragement, the needy bastard:

template<typename F, typename P> void weird() {
    typename F::template Nested<P>();
}

It’s not all bad—there’s been some progress in recent years on making these annotations optional in certain cases where there’s only one valid interpretation. For example, take a look at this case:

template<typename B> struct T : B::type {};

Technically, B::type is dependent on B, so traditional wisdom would suggest we ought to tack a typename onto B::type. However, since B::type must be a type for this program to be valid, the annotation is unnecessary (and, in fact, disallowed). That’s not new, though—what is new is the generalization of this reasoning to a few other cases where nothing but a type-based interpretation would make sense. One instance of this is for function return types:

// Valid no matter what:
template<typename T>
typename std::enable_if<true, T>::type foo() {}

// Valid only since C++20:
template<typename T>
std::enable_if<true, T>::type bar() {}

So, it really isn’t all bad. Unless you’re invoking a dependent function name. That shit still sucks. Oh well.