为什么此模板函数的行为不符合预期?

Why does this template function not behave as expected?

本文关键字:不符合 函数 为什么      更新时间:2023-10-16

我正在阅读有关模板函数的信息,并对此问题感到困惑:

#include <iostream>
void f(int) {
std::cout << "f(int)n";
}
template<typename T>
void g(T val) {
std::cout << typeid(val).name() << "  ";
f(val);
}
void f(double) {
std::cout << "f(double)n";
}
template void g<double>(double);
int main() {
f(1.0); // f(double)
f(1);   // f(int)
g(1.0); // d  f(int), this is surprising
g(1);   // i  f(int)
}

如果我不写template void g<double>(double);,结果是一样的.

我认为g<double>应该在f(double)之后实例化,因此g中对f的调用应该调用f(double)。令人惊讶的是,它仍然在g<double>中称f(int)。谁能帮我理解这一点?


看完答案后,我明白了我的困惑到底是什么。

下面是一个更新的示例。除了我为g<double>添加了专业化之外,它基本上没有变化:

#include <iostream>
void f(int){cout << "f(int)" << endl;}
template<typename T>
void g(T val)
{
cout << typeid(val).name() << "  ";
f(val);
}
void f(double){cout << "f(double)" << endl;}
//Now use user specialization to replace
//template void g<double>(double);
template<>
void g<double>(double val)
{
cout << typeid(val).name() << "  ";
f(val);
}
int main() {
f(1.0); // f(double)
f(1);  // f(int)
g(1.0); // now d  f(double)
g(1);  // i  f(int)
}

通过用户专业化,g(1.0)的行为符合我的预期。

编译器是否应该不自动对同一位置的g<double>执行相同的实例化(甚至在main()之后,如C++编程语言第 4 版第 26.3.3 节所述(?

名称f是一个依赖名称(它取决于通过参数valT(,它将解析为两个步骤:

  1. 非 ADL 查找检查函数声明...在模板定义上下文中可见。
  2. ADL 检查函数声明...从模板定义上下文或模板实例化上下文中可见。

void f(double)在模板定义上下文中不可见,ADL 也找不到它,因为

对于基本类型的参数,关联的命名空间和类集为空


我们可以稍微修改您的示例:

struct Int {};
struct Double : Int {};
void f(Int) { 
std::cout << "f(Int)";
}
template<typename T>
void g(T val) {
std::cout << typeid(val).name() << ' ';
f(val);
// (f)(val);
}
void f(Double) { 
std::cout << "f(Double)";
}
int main() {
g(Double{});
}

现在ADL将在第二步中找到void f(Double),输出将6Double f(Double)。我们可以通过编写(f)(val)(或::f(val)(而不是f(val)来禁用 ADL。然后输出将是6Double f(Int),与您的示例一致。

问题是f(double)在你调用它的地方没有被声明;如果你把它的声明移到template g前面,它就会被调用。

编辑:为什么要使用手动实例化?

(我将只讨论函数模板,类似的论证也适用于类模板。主要用途是减少编译时间和/或对用户隐藏模板的代码。

C++程序通过 2 个步骤构建到二进制文件中:编译和链接。要使函数调用的编译成功,只需要函数的标头。要使链接成功,需要一个包含函数编译主体的对象文件。

现在,当编译器看到模板函数的调用时,它执行的操作取决于它是知道模板的主体还是只知道标头。如果它只看到标头,则执行与函数未模板化相同的操作:将有关链接器调用的信息放入对象文件。但是,如果它也看到模板的主体,它也会做另一件事:它实例化主体的正确实例,编译这个主体并将其放入目标文件中。

如果多个源文件调用模板化函数的同一实例,则它们的每个目标文件都将包含函数实例的编译版本。(Linker 知道这一点,并将所有调用解析为单个编译函数,因此在程序/库的最终二进制文件中只有一个。但是,为了编译每个源文件,必须实例化和编译函数,这需要时间。

如果函数的主体位于一个对象文件中,则链接器完成其工作就足够了。在源文件中手动实例化模板是一种使编译器将函数主体放入相关源文件的目标文件中的方法。(这有点像调用了函数,但实例化是在函数调用无效的地方编写的。完成此操作后,可以编译调用函数的所有文件,只知道函数的标头,从而节省每次调用实例化和编译函数主体所需的时间。

第二个原因(实现隐藏(现在可能有意义。如果库作者希望其模板函数的用户能够使用该函数,她通常会为他们提供模板的代码,以便他们可以自己编译。如果她想对模板的源代码保密,她可以在她用来构建库的代码中手动实例化模板,并为用户提供由此获得的对象版本而不是源代码。

这有什么意义吗?