23

我正在阅读有关模板函数的内容,但对这个问题感到困惑:

#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),因此对fin的调用g应该调用f(double)。令人惊讶的是,它仍然调用f(int). g<double>谁能帮我理解这一点?


阅读答案后,我弄清楚了我的困惑到底是什么。

这是一个更新的示例。除了我添加了一个专业化之外,它几乎没有变化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(),如The C++ Programming Language , 4th edition 的第 26.3.3 节所述)?

4

2 回答 2

12

该名称f是一个依赖名称(它取决于T通过参数val),它将分为两个步骤

  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))而不是来禁用 ADL f(val)。然后输出将是6Double f(Int),与您的示例一致。

于 2019-11-29T06:47:51.363 回答
6

问题是f(double)在您调用它时尚未声明;如果您将其声明移到 前面template g,它将被调用。

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

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

C++ 程序分两步构建到二进制文件中:编译和链接。为了成功编译函数调用,只需要函数的标头。为了链接成功,需要一个包含函数编译体的目标文件。

现在,当编译器看到模板化函数的调用时,它会做什么取决于它是否知道模板的主体或仅知道标头。如果它只看到头文件,它会做与没有模板化函数一样的事情:将有关链接器调用的信息放到目标文件中。但如果它也看到了模板的主体,它还会做另一件事:它实例化主体的适当实例,编译该主体并将其也放入目标文件中。

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

如果函数的主体在一个目标文件中,链接器就足以完成它的工作。在源文件中手动实例化模板是一种让编译器将函数体放入相关源文件的目标文件中的方法。(有点好像函数被调用了,但是实例化写在函数调用无效的地方。)完成后,所有调用函数的文件都可以编译,只知道函数的头,因此节省使用每个调用实例化和编译函数体所需的时间。

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

这有道理吗?

于 2019-11-29T06:10:47.427 回答