【问题标题】:Why can I call function templates without forward declarations?为什么我可以在没有前向声明的情况下调用函数模板?
【发布时间】:2026-02-05 04:45:02
【问题描述】:

如果一个普通函数调用一个尚未声明的函数,我会得到一个编译时错误:

void foo(int x)
{
    bar(x);   // ERROR: bar has not been declared yet
}

void bar(int x)
{
    std::cout << x << '\n';
}

int main()
{
    foo(42);
}

解决方法是前向声明被调用的函数,或者切换定义的顺序。

但是,对于函数模板,这些修复似乎不是必需的:

template<typename T>
void foo(T x)
{
    bar(x);   // OKAY
}

template<typename T>
void bar(T x)
{
    std::cout << x << '\n';
}

int main()
{
    foo(42);
}

这编译得很好。这是为什么?编译器看到bar(x),为什么不报错?

(我使用的是 g++ 4.6.3)

【问题讨论】:

  • gcc 4.8.1 在这里。不编译。
  • @sbabbi:有什么错误?它在 gcc 4.8.1 上对我来说编译得很好。
  • @J.N. “错误:在这个范围内没有声明‘bar’,并且在实例化点[-fpermissive]没有通过依赖于参数的查找找到任何声明”
  • 您是否按原样复制样本?还是你改变了什么?
  • @J.N. “#include”和复制粘贴。它也无法在 ideone 上编译。

标签: c++ function templates gcc forward-declaration


【解决方案1】:

这是一个“为什么天空是砖砌成的”类型的问题。即,一个问题,问为什么假的东西是真的。在 C++ 中,您的代码不是合法的。

Live example,正如您在 gcc 4.8 中看到的那样,这实际上并没有编译。

我猜“为什么 gcc 4.6 让这段代码编译”的问题仍然存在。编译器在编写template 扩展器时所做的一件事是将它们视为类似于宏的东西。在声明它们时几乎不会做任何事情,并且在实例化它们时会查找所有内容。

编译器现在倾向于在声明 template 时做更多事情,而在实例化它时更少。这是 C++ 标准所要求的,或者至少更接近。

碰巧,ADL 可以解决这个问题:通过 ADL 找到 barbar 查找不必在写入 foo 时可见,而是在实例化时可见。

gcc 4.8 的错误信息很容易解释:

prog.cpp: In instantiation of ‘void foo(T) [with T = int]’:
prog.cpp:16:7:   required from here
prog.cpp:6:10: error: ‘bar’ was not declared in this scope, and no declarations were found by argument-dependent lookup at the point of instantiation [-fpermissive]
     bar(x);   // OKAY
          ^
prog.cpp:10:6: note: ‘template<class T> void bar(T)’ declared here, later in the translation unit
 void bar(T x)
      ^

这些要求在 C++11 中可能已经更改或澄清,因此 gcc 4.6 的行为可能在 C++03 标准下是合法的。

【讨论】:

    【解决方案2】:

    当编译器首先看到bar(x)时,它不知道x的类型,因此它无法查找正确的bar。只有当您实例化fooT 并因此知道x 的类型并且可以查找bar(x) 时。

    请注意,这仅适用于依赖表达式,即依赖于模板参数的表达式。如果加上bar(42),即使后面用T==int实例化也会编译失败。

    您可能还想通过谷歌搜索“两阶段查找”以获取更多信息。只有最新版本的 GCC 才能正确实现这些规则,因为在解析模板的第一阶段还需要进行一些检查。正如 Yakk 指出的那样,较新版本的 GCC 会拒绝您的代码,因此请务必检查最新版本的 GCC 或 Clang 以确保安全。

    【讨论】:

      【解决方案3】:

      函数模板不是函数;一旦知道模板参数,它就是制作函数的秘诀。

      编译器在看到foo 模板定义时无法查找bar 的含义,因为它的含义可能取决于T 是什么。所以它只记得有一个名称bar 的使用需要稍后解决。

      当您调用 foo(42) 时,编译必须生成 (instantiate) 真正的函数,然后它会查找以前无法查找的名称,找到您的 bar 模板 (并触发它的实例化),一切都很好。

      对于普通函数,所有名称都可以在定义函数时查找,因此必须在该点正确声明它们。

      【讨论】:

      • 我非常喜欢这个答案,但我接受它实际上并不正确——或者至少不完全正确。正如 Yakk 在他的回答中指出的那样,规则更复杂 - 在实例化时查找名称时只考虑 ADL,并且使用 int 不会触发在全局命名空间中查找 ADL,而全局命名空间是 bar 所在的位置。
      最近更新 更多