依赖名称,并稍加鼓励
Dependent Names with a Little Encouragement

原始链接: https://consteval.ca/2025/09/27/dependent-names/

这段文字详细描述了C++模板解析中一个特殊的角落,具体涉及“依赖名称”——依赖于模板参数的类成员。核心问题在于尝试调用一个可能本身就是模板的成员函数(例如`foo`)。编译器可能会将`<`符号误解为小于号比较运算符,而不是模板参数列表的开始,从而导致解析错误。 这个问题源于编译器在不知道类型`F`的情况下,无法确定`F::foo`是普通成员还是成员模板。为了解决这种歧义,C++需要使用`template`关键字进行显式引导:`f.template foo

();`。 类似的问题也会发生在嵌套类型和模板中,需要使用`typename`和`template`关键字来明确意图。最近的C++标准(C++20)在某些上下文中放宽了这些要求,即当上下文明确表明需要一个类型时,但依赖函数调用仍然需要显式注释。本质上,编译器需要帮助来正确解释涉及模板的潜在歧义代码。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 依赖名称与一点鼓励 (consteval.ca) 3 分,HeliumHydride 发表于 2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

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.

联系我们 contact @ memedata.com